From a723038b447edda365448e0f738f057b10d59f4d Mon Sep 17 00:00:00 2001 From: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Date: Fri, 12 Jul 2024 06:14:08 +1000 Subject: [PATCH 01/57] Remove OrganizationUserType.Manager (#10060) --- .../access-selector/access-selector.component.spec.ts | 2 +- .../src/admin-console/enums/organization-user-type.enum.ts | 6 +----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.component.spec.ts b/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.component.spec.ts index 90e652675c4..592995f88fc 100644 --- a/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.component.spec.ts +++ b/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.component.spec.ts @@ -205,7 +205,7 @@ describe("AccessSelectorComponent", () => { labelName: "Member 1", listName: "Member 1 (member1@email.com)", email: "member1@email.com", - role: OrganizationUserType.Manager, + role: OrganizationUserType.User, status: OrganizationUserStatusType.Confirmed, }, ]; diff --git a/libs/common/src/admin-console/enums/organization-user-type.enum.ts b/libs/common/src/admin-console/enums/organization-user-type.enum.ts index 657d2a4a6cb..da50bfbdc20 100644 --- a/libs/common/src/admin-console/enums/organization-user-type.enum.ts +++ b/libs/common/src/admin-console/enums/organization-user-type.enum.ts @@ -2,10 +2,6 @@ export enum OrganizationUserType { Owner = 0, Admin = 1, User = 2, - /** - * @deprecated - * This is deprecated with the introduction of Flexible Collections. - */ - Manager = 3, + // Manager = 3 has been intentionally permanently deleted Custom = 4, } From 050f8f4bdcb7a7b8e79a9b061d344aa25ed5bedf Mon Sep 17 00:00:00 2001 From: SmithThe4th Date: Thu, 11 Jul 2024 17:39:49 -0400 Subject: [PATCH 02/57] [PM-7624] [PM-7625] Bulk management actions on individual vault (#9507) * fixed issue with clearing search index state * clear user index before account is totally cleaned up * added logout clear on option * removed redundant clear index from logout * added feature flag * added new menu drop down and put behind feature flag * added permanentlyDeleteSelected to the menu * added permanentlyDeleteSelected to the menu * wired up logic to show to hide menu drop down items * modified the bulk collection assignment to work with end user vault * wired up delete and move to folder * merged bulk management actions header into old leveraging the feature flag * added ability to move personal items to an organization and set active collection when user is on a collection * made collection required by default * handled organization cipher share when personal items and org items are selected * moved logic to determine warning text to component class * moved logic to determine warning text to component class * Improved hide or show logic for menu * added bullet point to bulk assignment dialog content * changed description for move to folder * Fixed issue were all collections are retrived instead of only can manage, and added logic to get collections associated with a cipher * added inline assign to collections * added logic to disable three dot to template * Updated logic to retreive shared collection ids between ciphers * Added logic to make attachment view only, show or hide * Only show menu options when there are options available * Comments cleanup * update cipher row to disable menu instead of hide * Put add to folder behind feature flag * ensured old menu behaviour is shown when feature flag is turned off * refactored code base on code review suggestions * fixed bug with available collections * Made assign to collections resuable made pluralize a pipe instead * Utilized the resuable assign to collections component on the web * changed description message for collection assignment * fixed bug with ExpressionChangedAfterItHasBeenCheckedError * Added changedetectorref markForCheck * removed redundant startwith as seed value has been added * made code review suggestions * fixed bug where assign to collections shows up in trash filter * removed bitInput * refactored based on code review comments * added reference ticket * [PM-9341] Cannot assign to collections when filtering by My Vault (#9862) * Add checks for org id myvault * made myvault id a constant * show bulk move is set by individual vault and it is needed so assign to collections does not show up in trash filter (#9876) * Fixed issue where selectedOrgId is null (#9879) * Fix bug introduced with assigning items to a collection (#9897) * [PM-9601] [PM-9602] When collection management setting is turned on view only collections and assign to collections menu option show up (#10047) * Only show collections with edit access on individual vault * remove unused arguments --- .../assign-collections-web.component.html | 35 ++ .../assign-collections-web.component.ts | 39 ++ .../components/assign-collections/index.ts | 1 + .../vault-cipher-row.component.html | 38 +- .../vault-items/vault-cipher-row.component.ts | 53 +++ .../vault-items/vault-items.component.html | 29 +- .../vault-items/vault-items.component.ts | 116 +++++ .../individual-vault/attachments.component.ts | 1 - .../bulk-move-dialog.component.html | 11 +- .../bulk-move-dialog.component.ts | 7 + .../individual-vault/vault.component.html | 2 + .../vault/individual-vault/vault.component.ts | 71 ++- ...ollection-assignment-dialog.component.html | 66 --- ...-collection-assignment-dialog.component.ts | 195 -------- .../index.ts | 1 - .../app/vault/org-vault/vault.component.ts | 11 +- apps/web/src/locales/en/messages.json | 32 +- libs/angular/src/jslib.module.ts | 2 + libs/angular/src/pipes/pluralize.pipe.ts | 11 + .../vault/components/attachments.component.ts | 1 + libs/common/src/enums/feature-flag.enum.ts | 2 + .../assign-collections.component.html | 42 ++ .../assign-collections.component.ts | 443 ++++++++++++++++++ libs/vault/src/index.ts | 5 + 24 files changed, 919 insertions(+), 295 deletions(-) create mode 100644 apps/web/src/app/vault/components/assign-collections/assign-collections-web.component.html create mode 100644 apps/web/src/app/vault/components/assign-collections/assign-collections-web.component.ts create mode 100644 apps/web/src/app/vault/components/assign-collections/index.ts delete mode 100644 apps/web/src/app/vault/org-vault/bulk-collection-assignment-dialog/bulk-collection-assignment-dialog.component.html delete mode 100644 apps/web/src/app/vault/org-vault/bulk-collection-assignment-dialog/bulk-collection-assignment-dialog.component.ts delete mode 100644 apps/web/src/app/vault/org-vault/bulk-collection-assignment-dialog/index.ts create mode 100644 libs/angular/src/pipes/pluralize.pipe.ts create mode 100644 libs/vault/src/components/assign-collections.component.html create mode 100644 libs/vault/src/components/assign-collections.component.ts diff --git a/apps/web/src/app/vault/components/assign-collections/assign-collections-web.component.html b/apps/web/src/app/vault/components/assign-collections/assign-collections-web.component.html new file mode 100644 index 00000000000..f05262832c7 --- /dev/null +++ b/apps/web/src/app/vault/components/assign-collections/assign-collections-web.component.html @@ -0,0 +1,35 @@ + + + {{ "assignToCollections" | i18n }} + + {{ editableItemCount | pluralize: ("item" | i18n) : ("items" | i18n) }} + + + +
+ +
+ + + + + +
diff --git a/apps/web/src/app/vault/components/assign-collections/assign-collections-web.component.ts b/apps/web/src/app/vault/components/assign-collections/assign-collections-web.component.ts new file mode 100644 index 00000000000..dc7740cc240 --- /dev/null +++ b/apps/web/src/app/vault/components/assign-collections/assign-collections-web.component.ts @@ -0,0 +1,39 @@ +import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog"; +import { Component, Inject } from "@angular/core"; + +import { PluralizePipe } from "@bitwarden/angular/pipes/pluralize.pipe"; +import { DialogService } from "@bitwarden/components"; +import { + AssignCollectionsComponent, + CollectionAssignmentParams, + CollectionAssignmentResult, +} from "@bitwarden/vault"; + +import { SharedModule } from "../../../shared"; + +@Component({ + imports: [SharedModule, AssignCollectionsComponent, PluralizePipe], + templateUrl: "./assign-collections-web.component.html", + standalone: true, +}) +export class AssignCollectionsWebComponent { + protected loading = false; + protected disabled = false; + protected editableItemCount: number; + + constructor( + @Inject(DIALOG_DATA) public params: CollectionAssignmentParams, + private dialogRef: DialogRef, + ) {} + + protected async onCollectionAssign(result: CollectionAssignmentResult) { + this.dialogRef.close(result); + } + + static open(dialogService: DialogService, config: DialogConfig) { + return dialogService.open( + AssignCollectionsWebComponent, + config, + ); + } +} diff --git a/apps/web/src/app/vault/components/assign-collections/index.ts b/apps/web/src/app/vault/components/assign-collections/index.ts new file mode 100644 index 00000000000..0c20f958850 --- /dev/null +++ b/apps/web/src/app/vault/components/assign-collections/index.ts @@ -0,0 +1 @@ +export * from "./assign-collections-web.component"; diff --git a/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.html b/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.html index af2a8443edf..604dd4acadf 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.html +++ b/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.html @@ -69,8 +69,9 @@ - + - + + - + @@ -138,7 +155,12 @@ {{ "restore" | i18n }} - - @@ -125,6 +140,8 @@ [organizations]="allOrganizations" [collections]="allCollections" [checked]="selection.isSelected(item)" + [canEditCipher]="canEditCipher(item.cipher) && vaultBulkManagementActionEnabled" + [vaultBulkManagementActionEnabled]="vaultBulkManagementActionEnabled" (checkedToggled)="selection.toggle(item)" (onEvent)="event($event)" > diff --git a/apps/web/src/app/vault/components/vault-items/vault-items.component.ts b/apps/web/src/app/vault/components/vault-items/vault-items.component.ts index baca403f181..bfb30f3f769 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-items.component.ts +++ b/apps/web/src/app/vault/components/vault-items/vault-items.component.ts @@ -48,6 +48,7 @@ export class VaultItemsComponent { @Input() addAccessStatus: number; @Input() addAccessToggle: boolean; @Input() restrictProviderAccess: boolean; + @Input() vaultBulkManagementActionEnabled = false; private _ciphers?: CipherView[] = []; @Input() get ciphers(): CipherView[] { @@ -93,10 +94,24 @@ export class VaultItemsComponent { ); } + get disableMenu() { + return ( + this.vaultBulkManagementActionEnabled && + !this.bulkMoveAllowed && + !this.showAssignToCollections() && + !this.showDelete() + ); + } + get bulkAssignToCollectionsAllowed() { return this.showBulkAddToCollections && this.ciphers.length > 0; } + // Use new bulk management delete if vaultBulkManagementActionEnabled feature flag is enabled + get deleteAllowed() { + return this.vaultBulkManagementActionEnabled ? this.showDelete() : true; + } + protected canEditCollection(collection: CollectionView): boolean { // Only allow allow deletion if collection editing is enabled and not deleting "Unassigned" if (collection.id === Unassigned) { @@ -192,6 +207,22 @@ export class VaultItemsComponent { return false; } + protected canEditCipher(cipher: CipherView) { + if (cipher.organizationId == null) { + return true; + } + + const organization = this.allOrganizations.find((o) => o.id === cipher.organizationId); + return ( + (organization.canEditAllCiphers( + this.flexibleCollectionsV1Enabled, + this.restrictProviderAccess, + ) && + this.viewingOrgVault) || + cipher.edit + ); + } + private refreshItems() { const collections: VaultItem[] = this.collections.map((collection) => ({ collection })); const ciphers: VaultItem[] = this.ciphers.map((cipher) => ({ cipher })); @@ -235,4 +266,89 @@ export class VaultItemsComponent { .map((item) => item.cipher), }); } + + protected showAssignToCollections(): boolean { + if (!this.showBulkMove) { + return false; + } + + if (this.selection.selected.length === 0) { + return true; + } + + const hasPersonalItems = this.hasPersonalItems(); + const uniqueCipherOrgIds = this.getUniqueOrganizationIds(); + + // Return false if items are from different organizations + if (uniqueCipherOrgIds.size > 1) { + return false; + } + + // If all items are personal, return based on personal items + if (uniqueCipherOrgIds.size === 0) { + return hasPersonalItems; + } + + const [orgId] = uniqueCipherOrgIds; + const organization = this.allOrganizations.find((o) => o.id === orgId); + + const canEditOrManageAllCiphers = + organization?.canEditAllCiphers( + this.flexibleCollectionsV1Enabled, + this.restrictProviderAccess, + ) && this.viewingOrgVault; + + const collectionNotSelected = + this.selection.selected.filter((item) => item.collection).length === 0; + + return (canEditOrManageAllCiphers || this.allCiphersHaveEditAccess()) && collectionNotSelected; + } + + protected showDelete(): boolean { + if (this.selection.selected.length === 0) { + return true; + } + + const hasPersonalItems = this.hasPersonalItems(); + const uniqueCipherOrgIds = this.getUniqueOrganizationIds(); + const organizations = Array.from(uniqueCipherOrgIds, (orgId) => + this.allOrganizations.find((o) => o.id === orgId), + ); + + const canEditOrManageAllCiphers = + organizations.length > 0 && + organizations.every((org) => + org?.canEditAllCiphers(this.flexibleCollectionsV1Enabled, this.restrictProviderAccess), + ); + + const canDeleteCollections = this.selection.selected + .filter((item) => item.collection) + .every((item) => item.collection && this.canDeleteCollection(item.collection)); + + const userCanDeleteAccess = + (canEditOrManageAllCiphers || this.allCiphersHaveEditAccess()) && canDeleteCollections; + + if ( + userCanDeleteAccess || + (hasPersonalItems && (!uniqueCipherOrgIds.size || userCanDeleteAccess)) + ) { + return true; + } + + return false; + } + + private hasPersonalItems(): boolean { + return this.selection.selected.some(({ cipher }) => cipher?.organizationId === null); + } + + private allCiphersHaveEditAccess(): boolean { + return this.selection.selected + .filter(({ cipher }) => cipher) + .every(({ cipher }) => cipher?.edit); + } + + private getUniqueOrganizationIds(): Set { + return new Set(this.selection.selected.flatMap((i) => i.cipher?.organizationId ?? [])); + } } diff --git a/apps/web/src/app/vault/individual-vault/attachments.component.ts b/apps/web/src/app/vault/individual-vault/attachments.component.ts index ae4e8fafabe..3bf87ba4e3c 100644 --- a/apps/web/src/app/vault/individual-vault/attachments.component.ts +++ b/apps/web/src/app/vault/individual-vault/attachments.component.ts @@ -18,7 +18,6 @@ import { DialogService } from "@bitwarden/components"; templateUrl: "attachments.component.html", }) export class AttachmentsComponent extends BaseAttachmentsComponent { - viewOnly = false; protected override componentName = "app-vault-attachments"; constructor( diff --git a/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-move-dialog/bulk-move-dialog.component.html b/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-move-dialog/bulk-move-dialog.component.html index 8843bda2f7b..59341a712d5 100644 --- a/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-move-dialog/bulk-move-dialog.component.html +++ b/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-move-dialog/bulk-move-dialog.component.html @@ -1,15 +1,16 @@
- {{ "moveSelected" | i18n }} + {{ ((vaultBulkManagementActionEnabled$ | async) ? "addToFolder" : "moveSelected") | i18n }}

{{ "moveSelectedItemsDesc" | i18n: cipherIds.length }}

- {{ "folder" | i18n }} - + {{ "selectFolder" | i18n }} + + + +
diff --git a/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-move-dialog/bulk-move-dialog.component.ts b/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-move-dialog/bulk-move-dialog.component.ts index cdf45d0669c..252cdc7ac54 100644 --- a/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-move-dialog/bulk-move-dialog.component.ts +++ b/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-move-dialog/bulk-move-dialog.component.ts @@ -3,6 +3,8 @@ import { Component, Inject, OnInit } from "@angular/core"; import { FormBuilder, Validators } from "@angular/forms"; import { firstValueFrom, Observable } from "rxjs"; +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 { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; @@ -45,6 +47,10 @@ export class BulkMoveDialogComponent implements OnInit { }); folders$: Observable; + protected vaultBulkManagementActionEnabled$ = this.configService.getFeatureFlag$( + FeatureFlag.VaultBulkManagementAction, + ); + constructor( @Inject(DIALOG_DATA) params: BulkMoveDialogParams, private dialogRef: DialogRef, @@ -53,6 +59,7 @@ export class BulkMoveDialogComponent implements OnInit { private i18nService: I18nService, private folderService: FolderService, private formBuilder: FormBuilder, + private configService: ConfigService, ) { this.cipherIds = params.cipherIds ?? []; } diff --git a/apps/web/src/app/vault/individual-vault/vault.component.html b/apps/web/src/app/vault/individual-vault/vault.component.html index 780614c3303..fe1a97aff1d 100644 --- a/apps/web/src/app/vault/individual-vault/vault.component.html +++ b/apps/web/src/app/vault/individual-vault/vault.component.html @@ -50,8 +50,10 @@ [showBulkTrashOptions]="filter.type === 'trash'" [useEvents]="false" [showAdminActions]="false" + [showBulkAddToCollections]="vaultBulkManagementActionEnabled$ | async" (onEvent)="onVaultItemsEvent($event)" [flexibleCollectionsV1Enabled]="flexibleCollectionsV1Enabled$ | async" + [vaultBulkManagementActionEnabled]="vaultBulkManagementActionEnabled$ | async" >
(); private refresh$ = new BehaviorSubject(null); @@ -379,9 +384,7 @@ export class VaultComponent implements OnInit, OnDestroy { (o) => o.canCreateNewCollections && !o.isProviderUser, ); - this.showBulkMove = - filter.type !== "trash" && - (filter.organizationId === undefined || filter.organizationId === Unassigned); + this.showBulkMove = filter.type !== "trash"; this.isEmpty = collections?.length === 0 && ciphers?.length === 0; this.performingInitialLoad = false; @@ -428,6 +431,8 @@ export class VaultComponent implements OnInit, OnDestroy { await this.editCollection(event.item, CollectionDialogTabType.Info); } else if (event.type === "viewCollectionAccess") { await this.editCollection(event.item, CollectionDialogTabType.Access); + } else if (event.type === "assignToCollections") { + await this.bulkAssignToCollections(event.items); } } finally { this.processingEvent = false; @@ -492,12 +497,18 @@ export class VaultComponent implements OnInit, OnDestroy { } } + const canEditAttachments = await this.canEditAttachments(cipher); + const vaultBulkManagementActionEnabled = await firstValueFrom( + this.vaultBulkManagementActionEnabled$, + ); + let madeAttachmentChanges = false; const [modal] = await this.modalService.openViewRef( AttachmentsComponent, this.attachmentsModalRef, (comp) => { comp.cipherId = cipher.id; + comp.viewOnly = !canEditAttachments && vaultBulkManagementActionEnabled; comp.onUploadedAttachment .pipe(takeUntil(this.destroy$)) .subscribe(() => (madeAttachmentChanges = true)); @@ -707,6 +718,47 @@ export class VaultComponent implements OnInit, OnDestroy { } } + async bulkAssignToCollections(ciphers: CipherView[]) { + if (!(await this.repromptCipher(ciphers))) { + return; + } + + if (ciphers.length === 0) { + this.platformUtilsService.showToast( + "error", + this.i18nService.t("errorOccurred"), + this.i18nService.t("nothingSelected"), + ); + return; + } + + let availableCollections: CollectionView[] = []; + const orgId = + this.activeFilter.organizationId || + ciphers.find((c) => c.organizationId !== null)?.organizationId; + + if (orgId && orgId !== "MyVault") { + const organization = this.allOrganizations.find((o) => o.id === orgId); + availableCollections = this.allCollections.filter( + (c) => c.organizationId === organization.id && !c.readOnly, + ); + } + + const dialog = AssignCollectionsWebComponent.open(this.dialogService, { + data: { + ciphers, + organizationId: orgId as OrganizationId, + availableCollections, + activeCollection: this.activeFilter?.selectedCollectionNode?.node, + }, + }); + + const result = await lastValueFrom(dialog.closed); + if (result === CollectionAssignmentResult.Saved) { + this.refresh(); + } + } + async cloneCipher(cipher: CipherView) { if (cipher.login?.hasFido2Credentials) { const confirmed = await this.dialogService.openSimpleDialog({ @@ -984,6 +1036,17 @@ export class VaultComponent implements OnInit, OnDestroy { this.refresh$.next(); } + private async canEditAttachments(cipher: CipherView) { + if (cipher.organizationId == null || cipher.edit) { + return true; + } + + const flexibleCollectionsV1Enabled = await this.flexibleCollectionsV1Enabled(); + + const organization = this.allOrganizations.find((o) => o.id === cipher.organizationId); + return organization.canEditAllCiphers(flexibleCollectionsV1Enabled, false); + } + private go(queryParams: any = null) { if (queryParams == null) { queryParams = { diff --git a/apps/web/src/app/vault/org-vault/bulk-collection-assignment-dialog/bulk-collection-assignment-dialog.component.html b/apps/web/src/app/vault/org-vault/bulk-collection-assignment-dialog/bulk-collection-assignment-dialog.component.html deleted file mode 100644 index 520e8077880..00000000000 --- a/apps/web/src/app/vault/org-vault/bulk-collection-assignment-dialog/bulk-collection-assignment-dialog.component.html +++ /dev/null @@ -1,66 +0,0 @@ - - - {{ "assignToCollections" | i18n }} - - {{ pluralize(editableItemCount, "item", "items") }} - - - -
-

{{ "bulkCollectionAssignmentDialogDescription" | i18n }}

- -

- {{ "bulkCollectionAssignmentWarning" | i18n: totalItemCount : readonlyItemCount }} -

- -
- - {{ "selectCollectionsToAssign" | i18n }} - - -
- - - - {{ "assignToTheseCollections" | i18n }} - - - - - - - {{ item.labelName }} - - - - - - - - {{ "noCollectionsAssigned" | i18n }} - - - - -
- - - - - -
diff --git a/apps/web/src/app/vault/org-vault/bulk-collection-assignment-dialog/bulk-collection-assignment-dialog.component.ts b/apps/web/src/app/vault/org-vault/bulk-collection-assignment-dialog/bulk-collection-assignment-dialog.component.ts deleted file mode 100644 index 8998629b665..00000000000 --- a/apps/web/src/app/vault/org-vault/bulk-collection-assignment-dialog/bulk-collection-assignment-dialog.component.ts +++ /dev/null @@ -1,195 +0,0 @@ -import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog"; -import { Component, Inject, OnDestroy, OnInit } from "@angular/core"; -import { Subject } from "rxjs"; - -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; -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 { CipherId, CollectionId, OrganizationId } 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 { CollectionView } from "@bitwarden/common/vault/models/view/collection.view"; -import { DialogService, SelectItemView } from "@bitwarden/components"; - -import { SharedModule } from "../../../shared"; - -export interface BulkCollectionAssignmentDialogParams { - organizationId: OrganizationId; - - /** - * The ciphers to be assigned to the collections selected in the dialog. - */ - ciphers: CipherView[]; - - /** - * The collections available to assign the ciphers to. - */ - availableCollections: CollectionView[]; - - /** - * The currently filtered collection. Selected by default. If the user deselects it in the dialog then it will be - * removed from the ciphers upon submission. - */ - activeCollection?: CollectionView; -} - -export enum BulkCollectionAssignmentDialogResult { - Saved = "saved", - Canceled = "canceled", -} - -@Component({ - imports: [SharedModule], - selector: "app-bulk-collection-assignment-dialog", - templateUrl: "./bulk-collection-assignment-dialog.component.html", - standalone: true, -}) -export class BulkCollectionAssignmentDialogComponent implements OnDestroy, OnInit { - protected totalItemCount: number; - protected editableItemCount: number; - protected readonlyItemCount: number; - protected availableCollections: SelectItemView[] = []; - protected selectedCollections: SelectItemView[] = []; - - private editableItems: CipherView[] = []; - private destroy$ = new Subject(); - - protected pluralize = (count: number, singular: string, plural: string) => - `${count} ${this.i18nService.t(count === 1 ? singular : plural)}`; - - constructor( - @Inject(DIALOG_DATA) private params: BulkCollectionAssignmentDialogParams, - private dialogRef: DialogRef, - private cipherService: CipherService, - private i18nService: I18nService, - private platformUtilsService: PlatformUtilsService, - private configService: ConfigService, - private organizationService: OrganizationService, - ) {} - - async ngOnInit() { - // If no ciphers are passed in, close the dialog - if (this.params.ciphers == null || this.params.ciphers.length < 1) { - this.platformUtilsService.showToast("error", null, this.i18nService.t("nothingSelected")); - this.dialogRef.close(BulkCollectionAssignmentDialogResult.Canceled); - return; - } - - const v1FCEnabled = await this.configService.getFeatureFlag(FeatureFlag.FlexibleCollectionsV1); - const restrictProviderAccess = await this.configService.getFeatureFlag( - FeatureFlag.RestrictProviderAccess, - ); - const org = await this.organizationService.get(this.params.organizationId); - - if (org.canEditAllCiphers(v1FCEnabled, restrictProviderAccess)) { - this.editableItems = this.params.ciphers; - } else { - this.editableItems = this.params.ciphers.filter((c) => c.edit); - } - - this.editableItemCount = this.editableItems.length; - - // If no ciphers are editable, close the dialog - if (this.editableItemCount == 0) { - this.platformUtilsService.showToast("error", null, this.i18nService.t("missingPermissions")); - this.dialogRef.close(BulkCollectionAssignmentDialogResult.Canceled); - return; - } - - this.totalItemCount = this.params.ciphers.length; - this.readonlyItemCount = this.totalItemCount - this.editableItemCount; - - this.availableCollections = this.params.availableCollections.map((c) => ({ - icon: "bwi-collection", - id: c.id, - labelName: c.name, - listName: c.name, - })); - - // If the active collection is set, select it by default - if (this.params.activeCollection) { - this.selectCollections([ - { - icon: "bwi-collection", - id: this.params.activeCollection.id, - labelName: this.params.activeCollection.name, - listName: this.params.activeCollection.name, - }, - ]); - } - } - - private sortItems = (a: SelectItemView, b: SelectItemView) => - this.i18nService.collator.compare(a.labelName, b.labelName); - - selectCollections(items: SelectItemView[]) { - this.selectedCollections = [...this.selectedCollections, ...items].sort(this.sortItems); - - this.availableCollections = this.availableCollections.filter( - (item) => !items.find((i) => i.id === item.id), - ); - } - - unselectCollection(i: number) { - const removed = this.selectedCollections.splice(i, 1); - this.availableCollections = [...this.availableCollections, ...removed].sort(this.sortItems); - } - - get isValid() { - return this.params.activeCollection != null || this.selectedCollections.length > 0; - } - - submit = async () => { - if (!this.isValid) { - return; - } - - const cipherIds = this.editableItems.map((i) => i.id as CipherId); - - if (this.selectedCollections.length > 0) { - await this.cipherService.bulkUpdateCollectionsWithServer( - this.params.organizationId, - cipherIds, - this.selectedCollections.map((i) => i.id as CollectionId), - false, - ); - } - - if ( - this.params.activeCollection != null && - this.selectedCollections.find((c) => c.id === this.params.activeCollection.id) == null - ) { - await this.cipherService.bulkUpdateCollectionsWithServer( - this.params.organizationId, - cipherIds, - [this.params.activeCollection.id as CollectionId], - true, - ); - } - - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("successfullyAssignedCollections"), - ); - - this.dialogRef.close(BulkCollectionAssignmentDialogResult.Saved); - }; - - ngOnDestroy(): void { - this.destroy$.next(); - this.destroy$.complete(); - } - - static open( - dialogService: DialogService, - config: DialogConfig, - ) { - return dialogService.open< - BulkCollectionAssignmentDialogResult, - BulkCollectionAssignmentDialogParams - >(BulkCollectionAssignmentDialogComponent, config); - } -} diff --git a/apps/web/src/app/vault/org-vault/bulk-collection-assignment-dialog/index.ts b/apps/web/src/app/vault/org-vault/bulk-collection-assignment-dialog/index.ts deleted file mode 100644 index 44042e3267a..00000000000 --- a/apps/web/src/app/vault/org-vault/bulk-collection-assignment-dialog/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./bulk-collection-assignment-dialog.component"; diff --git a/apps/web/src/app/vault/org-vault/vault.component.ts b/apps/web/src/app/vault/org-vault/vault.component.ts index 6622882bf86..07d65656d2f 100644 --- a/apps/web/src/app/vault/org-vault/vault.component.ts +++ b/apps/web/src/app/vault/org-vault/vault.component.ts @@ -59,12 +59,13 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view"; import { ServiceUtils } from "@bitwarden/common/vault/service-utils"; import { DialogService, Icons, ToastService } from "@bitwarden/components"; -import { PasswordRepromptService } from "@bitwarden/vault"; +import { CollectionAssignmentResult, PasswordRepromptService } from "@bitwarden/vault"; import { GroupService, GroupView } from "../../admin-console/organizations/core"; import { openEntityEventsDialog } from "../../admin-console/organizations/manage/entity-events.component"; import { VaultFilterService } from "../../vault/individual-vault/vault-filter/services/abstractions/vault-filter.service"; import { VaultFilter } from "../../vault/individual-vault/vault-filter/shared/models/vault-filter.model"; +import { AssignCollectionsWebComponent } from "../components/assign-collections"; import { CollectionDialogAction, CollectionDialogTabType, @@ -90,10 +91,6 @@ import { getNestedCollectionTree } from "../utils/collection-utils"; import { AddEditComponent } from "./add-edit.component"; import { AttachmentsComponent } from "./attachments.component"; -import { - BulkCollectionAssignmentDialogComponent, - BulkCollectionAssignmentDialogResult, -} from "./bulk-collection-assignment-dialog"; import { BulkCollectionsDialogComponent, BulkCollectionsDialogResult, @@ -1327,7 +1324,7 @@ export class VaultComponent implements OnInit, OnDestroy { ).filter((c) => c.id != Unassigned); } - const dialog = BulkCollectionAssignmentDialogComponent.open(this.dialogService, { + const dialog = AssignCollectionsWebComponent.open(this.dialogService, { data: { ciphers: items, organizationId: this.organization?.id as OrganizationId, @@ -1337,7 +1334,7 @@ export class VaultComponent implements OnInit, OnDestroy { }); const result = await lastValueFrom(dialog.closed); - if (result === BulkCollectionAssignmentDialogResult.Saved) { + if (result === CollectionAssignmentResult.Saved) { this.refresh(); } } diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index dbbae60cf70..f5875601189 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -1060,7 +1060,7 @@ "message": "Are you sure you want to continue?" }, "moveSelectedItemsDesc": { - "message": "Choose a folder that you would like to move the $COUNT$ selected item(s) to.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -7883,7 +7883,7 @@ "message": "Assign to these collections" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Select collections to assign" @@ -8547,5 +8547,33 @@ }, "licenseAndBillingManagementDesc": { "message": "After making updates in the Bitwarden cloud server, upload your license file to apply the most recent changes." + }, + "addToFolder": { + "message": "Add to folder" + }, + "selectFolder": { + "message": "Select folder" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/libs/angular/src/jslib.module.ts b/libs/angular/src/jslib.module.ts index 2ec13ea35e6..da8a4dd4181 100644 --- a/libs/angular/src/jslib.module.ts +++ b/libs/angular/src/jslib.module.ts @@ -46,6 +46,7 @@ import { StopClickDirective } from "./directives/stop-click.directive"; import { StopPropDirective } from "./directives/stop-prop.directive"; import { TrueFalseValueDirective } from "./directives/true-false-value.directive"; import { CreditCardNumberPipe } from "./pipes/credit-card-number.pipe"; +import { PluralizePipe } from "./pipes/pluralize.pipe"; import { SearchCiphersPipe } from "./pipes/search-ciphers.pipe"; import { SearchPipe } from "./pipes/search.pipe"; import { UserNamePipe } from "./pipes/user-name.pipe"; @@ -162,6 +163,7 @@ import { IconComponent } from "./vault/components/icon.component"; UserNamePipe, UserTypePipe, FingerprintPipe, + PluralizePipe, ], }) export class JslibModule {} diff --git a/libs/angular/src/pipes/pluralize.pipe.ts b/libs/angular/src/pipes/pluralize.pipe.ts new file mode 100644 index 00000000000..cc3aa3e0aa7 --- /dev/null +++ b/libs/angular/src/pipes/pluralize.pipe.ts @@ -0,0 +1,11 @@ +import { Pipe, PipeTransform } from "@angular/core"; + +@Pipe({ + name: "pluralize", + standalone: true, +}) +export class PluralizePipe implements PipeTransform { + transform(count: number, singular: string, plural: string): string { + return `${count} ${count === 1 ? singular : plural}`; + } +} diff --git a/libs/angular/src/vault/components/attachments.component.ts b/libs/angular/src/vault/components/attachments.component.ts index fc86f2f5277..68b336a8b06 100644 --- a/libs/angular/src/vault/components/attachments.component.ts +++ b/libs/angular/src/vault/components/attachments.component.ts @@ -20,6 +20,7 @@ import { DialogService } from "@bitwarden/components"; @Directive() export class AttachmentsComponent implements OnInit { @Input() cipherId: string; + @Input() viewOnly: boolean; @Output() onUploadedAttachment = new EventEmitter(); @Output() onDeletedAttachment = new EventEmitter(); @Output() onReuploadedAttachment = new EventEmitter(); diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index de387480f7e..3f451e38b19 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -23,6 +23,7 @@ export enum FeatureFlag { EnableTimeThreshold = "PM-5864-dollar-threshold", GroupsComponentRefactor = "groups-component-refactor", ProviderClientVaultPrivacyBanner = "ac-2833-provider-client-vault-privacy-banner", + VaultBulkManagementAction = "vault-bulk-management-action", } export type AllowedFeatureFlagTypes = boolean | number | string; @@ -56,6 +57,7 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.EnableTimeThreshold]: FALSE, [FeatureFlag.GroupsComponentRefactor]: FALSE, [FeatureFlag.ProviderClientVaultPrivacyBanner]: FALSE, + [FeatureFlag.VaultBulkManagementAction]: FALSE, } satisfies Record; export type DefaultFeatureFlagValueType = typeof DefaultFeatureFlagValue; diff --git a/libs/vault/src/components/assign-collections.component.html b/libs/vault/src/components/assign-collections.component.html new file mode 100644 index 00000000000..280acae1daa --- /dev/null +++ b/libs/vault/src/components/assign-collections.component.html @@ -0,0 +1,42 @@ + +

{{ "bulkCollectionAssignmentDialogDescription" | i18n }}

+ +
    +
  • +

    + {{ "bulkCollectionAssignmentWarning" | i18n: totalItemCount : readonlyItemCount }} +

    +
  • +
  • +

    + {{ transferWarningText(orgName, personalItemsCount) }} +

    +
  • +
+ +
+ + {{ "moveToOrganization" | i18n }} + + + + + +
+ +
+ + {{ "selectCollectionsToAssign" | i18n }} + + +
+ diff --git a/libs/vault/src/components/assign-collections.component.ts b/libs/vault/src/components/assign-collections.component.ts new file mode 100644 index 00000000000..5bbe616c639 --- /dev/null +++ b/libs/vault/src/components/assign-collections.component.ts @@ -0,0 +1,443 @@ +import { CommonModule } from "@angular/common"; +import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from "@angular/core"; +import { FormBuilder, ReactiveFormsModule, Validators } from "@angular/forms"; +import { + Observable, + Subject, + combineLatest, + map, + shareReplay, + switchMap, + takeUntil, + tap, +} from "rxjs"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { PluralizePipe } from "@bitwarden/angular/pipes/pluralize.pipe"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { OrganizationUserStatusType } 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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { CipherId, CollectionId, OrganizationId } from "@bitwarden/common/types/guid"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view"; +import { + AsyncActionsModule, + BitSubmitDirective, + ButtonModule, + DialogModule, + FormFieldModule, + MultiSelectModule, + SelectItemView, + SelectModule, + ToastService, +} from "@bitwarden/components"; + +export interface CollectionAssignmentParams { + organizationId: OrganizationId; + + /** + * The ciphers to be assigned to the collections selected in the dialog. + */ + ciphers: CipherView[]; + + /** + * The collections available to assign the ciphers to. + */ + availableCollections: CollectionView[]; + + /** + * The currently filtered collection. Selected by default. If the user deselects it in the dialog then it will be + * removed from the ciphers upon submission. + */ + activeCollection?: CollectionView; +} + +export enum CollectionAssignmentResult { + Saved = "saved", + Canceled = "canceled", +} + +const MY_VAULT_ID = "MyVault"; + +@Component({ + selector: "assign-collections", + templateUrl: "assign-collections.component.html", + standalone: true, + imports: [ + CommonModule, + JslibModule, + FormFieldModule, + AsyncActionsModule, + MultiSelectModule, + SelectModule, + ReactiveFormsModule, + ButtonModule, + DialogModule, + ], +}) +export class AssignCollectionsComponent implements OnInit { + @ViewChild(BitSubmitDirective) + private bitSubmit: BitSubmitDirective; + + @Input() params: CollectionAssignmentParams; + + @Output() + formLoading = new EventEmitter(); + + @Output() + formDisabled = new EventEmitter(); + + @Output() + editableItemCountChange = new EventEmitter(); + + @Output() onCollectionAssign = new EventEmitter(); + + formGroup = this.formBuilder.group({ + selectedOrg: [null], + collections: [[], [Validators.required]], + }); + + protected totalItemCount: number; + protected editableItemCount: number; + protected readonlyItemCount: number; + protected personalItemsCount: number; + protected availableCollections: SelectItemView[] = []; + protected orgName: string; + protected showOrgSelector: boolean = false; + + protected organizations$: Observable = + this.organizationService.organizations$.pipe( + map((orgs) => + orgs + .filter((o) => o.enabled && o.status === OrganizationUserStatusType.Confirmed) + .sort((a, b) => a.name.localeCompare(b.name)), + ), + tap((orgs) => { + if (orgs.length > 0 && this.showOrgSelector) { + // Using setTimeout to defer the patchValue call until the next event loop cycle + setTimeout(() => { + this.formGroup.patchValue({ selectedOrg: orgs[0].id }); + this.setFormValidators(); + }); + } + }), + ); + + protected transferWarningText = (orgName: string, itemsCount: number) => { + const pluralizedItems = this.pluralizePipe.transform(itemsCount, "item", "items"); + return orgName + ? this.i18nService.t("personalItemsWithOrgTransferWarning", pluralizedItems, orgName) + : this.i18nService.t("personalItemsTransferWarning", pluralizedItems); + }; + + private editableItems: CipherView[] = []; + // Get the selected organization ID. If the user has not selected an organization from the form, + // fallback to use the organization ID from the params. + private get selectedOrgId(): OrganizationId { + return this.formGroup.value.selectedOrg || this.params.organizationId; + } + private destroy$ = new Subject(); + + constructor( + private cipherService: CipherService, + private i18nService: I18nService, + private configService: ConfigService, + private organizationService: OrganizationService, + private collectionService: CollectionService, + private formBuilder: FormBuilder, + private pluralizePipe: PluralizePipe, + private toastService: ToastService, + ) {} + + async ngOnInit() { + const v1FCEnabled = await this.configService.getFeatureFlag(FeatureFlag.FlexibleCollectionsV1); + const restrictProviderAccess = await this.configService.getFeatureFlag( + FeatureFlag.RestrictProviderAccess, + ); + + const onlyPersonalItems = this.params.ciphers.every((c) => c.organizationId == null); + + if (this.selectedOrgId === MY_VAULT_ID || onlyPersonalItems) { + this.showOrgSelector = true; + } + + await this.initializeItems(this.selectedOrgId, v1FCEnabled, restrictProviderAccess); + + if (this.selectedOrgId && this.selectedOrgId !== MY_VAULT_ID) { + await this.handleOrganizationCiphers(); + } + + this.setupFormSubscriptions(); + } + + ngAfterViewInit(): void { + this.bitSubmit.loading$.pipe(takeUntil(this.destroy$)).subscribe((loading) => { + this.formLoading.emit(loading); + }); + + this.bitSubmit.disabled$.pipe(takeUntil(this.destroy$)).subscribe((disabled) => { + this.formDisabled.emit(disabled); + }); + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } + + selectCollections(items: SelectItemView[]) { + const currentCollections = this.formGroup.controls.collections.value as SelectItemView[]; + const updatedCollections = [...currentCollections, ...items].sort(this.sortItems); + this.formGroup.patchValue({ collections: updatedCollections }); + } + + submit = async () => { + this.formGroup.markAllAsTouched(); + + if (this.formGroup.invalid) { + return; + } + + // Retrieve ciphers that belong to an organization + const cipherIds = this.editableItems + .filter((i) => i.organizationId) + .map((i) => i.id as CipherId); + + // Move personal items to the organization + if (this.personalItemsCount > 0) { + await this.moveToOrganization( + this.selectedOrgId, + this.params.ciphers.filter((c) => c.organizationId == null), + this.formGroup.controls.collections.value.map((i) => i.id as CollectionId), + ); + } + + if (cipherIds.length > 0) { + const isSingleOrgCipher = cipherIds.length === 1 && this.personalItemsCount === 0; + + // Update assigned collections for single org cipher or bulk update collections for multiple org ciphers + await (isSingleOrgCipher + ? this.updateAssignedCollections(this.editableItems[0]) + : this.bulkUpdateCollections(cipherIds)); + + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("successfullyAssignedCollections"), + }); + } + + this.onCollectionAssign.emit(CollectionAssignmentResult.Saved); + }; + + private sortItems = (a: SelectItemView, b: SelectItemView) => + this.i18nService.collator.compare(a.labelName, b.labelName); + + private async handleOrganizationCiphers() { + // If no ciphers are editable, cancel the operation + if (this.editableItemCount == 0) { + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("nothingSelected"), + }); + this.onCollectionAssign.emit(CollectionAssignmentResult.Canceled); + + return; + } + + this.availableCollections = this.params.availableCollections.map((c) => ({ + icon: "bwi-collection", + id: c.id, + labelName: c.name, + listName: c.name, + })); + + // Select assigned collections for a single cipher. + this.selectCollectionsAssignedToSingleCipher(); + + // If the active collection is set, select it by default + if (this.params.activeCollection) { + this.selectCollections([ + { + icon: "bwi-collection", + id: this.params.activeCollection.id, + labelName: this.params.activeCollection.name, + listName: this.params.activeCollection.name, + }, + ]); + } + } + + /** + * Selects the collections that are assigned to a single cipher, + * excluding the active collection. + */ + private selectCollectionsAssignedToSingleCipher() { + if (this.params.ciphers.length !== 1) { + return; + } + + const assignedCollectionIds = this.params.ciphers[0].collectionIds; + + // Filter the available collections to select only those that are associated with the ciphers, excluding the active collection + const assignedCollections = this.availableCollections + .filter( + (collection) => + assignedCollectionIds.includes(collection.id) && + collection.id !== this.params.activeCollection?.id, + ) + .map((collection) => ({ + icon: "bwi-collection", + id: collection.id, + labelName: collection.labelName, + listName: collection.listName, + })); + + if (assignedCollections.length > 0) { + this.selectCollections(assignedCollections); + } + } + + private async initializeItems( + organizationId: OrganizationId, + v1FCEnabled: boolean, + restrictProviderAccess: boolean, + ) { + this.totalItemCount = this.params.ciphers.length; + + // If organizationId is not present or organizationId is MyVault, then all ciphers are considered personal items + if (!organizationId || organizationId === MY_VAULT_ID) { + this.editableItems = this.params.ciphers; + this.editableItemCount = this.params.ciphers.length; + this.personalItemsCount = this.params.ciphers.length; + this.editableItemCountChange.emit(this.editableItemCount); + return; + } + + const org = await this.organizationService.get(organizationId); + this.orgName = org.name; + + this.editableItems = org.canEditAllCiphers(v1FCEnabled, restrictProviderAccess) + ? this.params.ciphers + : this.params.ciphers.filter((c) => c.edit); + + this.editableItemCount = this.editableItems.length; + // TODO: https://bitwarden.atlassian.net/browse/PM-9307, + // clean up editableItemCountChange when the org vault is updated to filter editable ciphers + this.editableItemCountChange.emit(this.editableItemCount); + this.personalItemsCount = this.params.ciphers.filter((c) => c.organizationId == null).length; + this.readonlyItemCount = this.totalItemCount - this.editableItemCount; + } + + private setFormValidators() { + const selectedOrgControl = this.formGroup.get("selectedOrg"); + selectedOrgControl?.setValidators([Validators.required]); + selectedOrgControl?.updateValueAndValidity(); + } + + /** + * Sets up form subscriptions for selected organizations. + */ + private setupFormSubscriptions() { + // Listen to changes in selected organization and update collections + this.formGroup.controls.selectedOrg.valueChanges + .pipe( + tap(() => { + this.formGroup.controls.collections.setValue([], { emitEvent: false }); + }), + switchMap((orgId) => { + return this.getCollectionsForOrganization(orgId as OrganizationId); + }), + takeUntil(this.destroy$), + ) + .subscribe((collections) => { + this.availableCollections = collections.map((c) => ({ + icon: "bwi-collection", + id: c.id, + labelName: c.name, + listName: c.name, + })); + }); + } + + /** + * Retrieves the collections for the organization with the given ID. + * @param orgId + * @returns An observable of the collections for the organization. + */ + private getCollectionsForOrganization(orgId: OrganizationId): Observable { + return combineLatest([ + this.collectionService.decryptedCollections$, + this.organizationService.organizations$, + ]).pipe( + map(([collections, organizations]) => { + const org = organizations.find((o) => o.id === orgId); + this.orgName = org.name; + + return collections.filter((c) => { + return c.organizationId === orgId && !c.readOnly; + }); + }), + shareReplay({ refCount: true, bufferSize: 1 }), + ); + } + + private async moveToOrganization( + organizationId: OrganizationId, + shareableCiphers: CipherView[], + selectedCollectionIds: CollectionId[], + ) { + await this.cipherService.shareManyWithServer( + shareableCiphers, + organizationId, + selectedCollectionIds, + ); + + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t( + "movedItemsToOrg", + this.orgName ?? this.i18nService.t("organization"), + ), + }); + } + + private async bulkUpdateCollections(cipherIds: CipherId[]) { + if (this.formGroup.controls.collections.value.length > 0) { + await this.cipherService.bulkUpdateCollectionsWithServer( + this.selectedOrgId, + cipherIds, + this.formGroup.controls.collections.value.map((i) => i.id as CollectionId), + false, + ); + } + + if ( + this.params.activeCollection != null && + this.formGroup.controls.collections.value.find( + (c) => c.id === this.params.activeCollection.id, + ) == null + ) { + await this.cipherService.bulkUpdateCollectionsWithServer( + this.selectedOrgId, + cipherIds, + [this.params.activeCollection.id as CollectionId], + true, + ); + } + } + + private async updateAssignedCollections(cipherView: CipherView) { + const { collections } = this.formGroup.getRawValue(); + cipherView.collectionIds = collections.map((i) => i.id as CollectionId); + const cipher = await this.cipherService.encrypt(cipherView); + await this.cipherService.saveCollectionsWithServer(cipher); + } +} diff --git a/libs/vault/src/index.ts b/libs/vault/src/index.ts index e4e17e7aa5a..5dee70ea46f 100644 --- a/libs/vault/src/index.ts +++ b/libs/vault/src/index.ts @@ -4,3 +4,8 @@ export { CopyCipherFieldDirective } from "./components/copy-cipher-field.directi export * from "./cipher-view"; export * from "./cipher-form"; +export { + AssignCollectionsComponent, + CollectionAssignmentParams, + CollectionAssignmentResult, +} from "./components/assign-collections.component"; From 9dda29fb9cc58da32b17ad589c5f48c71763974f Mon Sep 17 00:00:00 2001 From: Shane Melton Date: Thu, 11 Jul 2024 15:01:24 -0700 Subject: [PATCH 03/57] [PM-7896] Cipher Form - Additional Options section (#9928) * [PM-7896] Adjust cipher form container to expose config and original cipher view for children * [PM-7896] Add initial additional options section * [PM-7896] Add tests * [PM-7896] Add TODO comments for Custom Fields * [PM-7896] Hide password reprompt checkbox when unavailable * [PM-7896] Fix storybook --- .../src/cipher-form/cipher-form-container.ts | 16 ++- .../src/cipher-form/cipher-form.stories.ts | 10 +- .../additional-options-section.component.html | 20 ++++ ...ditional-options-section.component.spec.ts | 99 +++++++++++++++++++ .../additional-options-section.component.ts | 75 ++++++++++++++ .../components/cipher-form.component.html | 2 + .../components/cipher-form.component.ts | 13 +-- .../src/services/password-reprompt.service.ts | 11 ++- 8 files changed, 235 insertions(+), 11 deletions(-) create mode 100644 libs/vault/src/cipher-form/components/additional-options/additional-options-section.component.html create mode 100644 libs/vault/src/cipher-form/components/additional-options/additional-options-section.component.spec.ts create mode 100644 libs/vault/src/cipher-form/components/additional-options/additional-options-section.component.ts diff --git a/libs/vault/src/cipher-form/cipher-form-container.ts b/libs/vault/src/cipher-form/cipher-form-container.ts index 0e4e34a6be6..a002e39d3e0 100644 --- a/libs/vault/src/cipher-form/cipher-form-container.ts +++ b/libs/vault/src/cipher-form/cipher-form-container.ts @@ -1,5 +1,7 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { CipherFormConfig } from "@bitwarden/vault"; +import { AdditionalOptionsSectionComponent } from "./components/additional-options/additional-options-section.component"; import { CardDetailsSectionComponent } from "./components/card-details-section/card-details-section.component"; import { IdentitySectionComponent } from "./components/identity/identity.component"; import { ItemDetailsSectionComponent } from "./components/item-details/item-details-section.component"; @@ -10,19 +12,31 @@ import { ItemDetailsSectionComponent } from "./components/item-details/item-deta */ export type CipherForm = { itemDetails?: ItemDetailsSectionComponent["itemDetailsForm"]; + additionalOptions?: AdditionalOptionsSectionComponent["additionalOptionsForm"]; cardDetails?: CardDetailsSectionComponent["cardDetailsForm"]; identityDetails?: IdentitySectionComponent["identityForm"]; }; /** * A container for the {@link CipherForm} that allows for registration of child form groups and patching of the cipher - * to be updated/created. Child form components inject this container in order to register themselves with the parent form. + * to be updated/created. Child form components inject this container in order to register themselves with the parent form + * and access configuration options. * * This is an alternative to passing the form groups down through the component tree via @Inputs() and form updates via * @Outputs(). It allows child forms to define their own structure and validation rules, while still being able to * update the parent cipher. */ export abstract class CipherFormContainer { + /** + * The configuration for the cipher form. + */ + readonly config: CipherFormConfig; + + /** + * The original cipher that is being edited/cloned. Used to pre-populate the form and compare changes. + */ + readonly originalCipherView: CipherView | null; + abstract registerChildForm( name: K, group: Exclude, diff --git a/libs/vault/src/cipher-form/cipher-form.stories.ts b/libs/vault/src/cipher-form/cipher-form.stories.ts index 47a1e90abcf..67011b5a478 100644 --- a/libs/vault/src/cipher-form/cipher-form.stories.ts +++ b/libs/vault/src/cipher-form/cipher-form.stories.ts @@ -7,6 +7,7 @@ import { moduleMetadata, StoryObj, } from "@storybook/angular"; +import { BehaviorSubject } from "rxjs"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { CipherType } from "@bitwarden/common/vault/enums"; @@ -15,7 +16,7 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; import { AsyncActionsModule, ButtonModule, ToastService } from "@bitwarden/components"; -import { CipherFormConfig } from "@bitwarden/vault"; +import { CipherFormConfig, PasswordRepromptService } from "@bitwarden/vault"; import { PreloadedEnglishI18nModule } from "@bitwarden/web-vault/src/app/core/tests"; import { CipherFormService } from "./abstractions/cipher-form.service"; @@ -71,6 +72,7 @@ const defaultConfig: CipherFormConfig = { folderId: "folder2", collectionIds: ["col1"], favorite: false, + notes: "Example notes", } as unknown as Cipher, }; @@ -105,6 +107,12 @@ export default { showToast: action("showToast"), }, }, + { + provide: PasswordRepromptService, + useValue: { + enabled$: new BehaviorSubject(true), + }, + }, ], }), componentWrapperDecorator( diff --git a/libs/vault/src/cipher-form/components/additional-options/additional-options-section.component.html b/libs/vault/src/cipher-form/components/additional-options/additional-options-section.component.html new file mode 100644 index 00000000000..d9c3a002048 --- /dev/null +++ b/libs/vault/src/cipher-form/components/additional-options/additional-options-section.component.html @@ -0,0 +1,20 @@ + + +

{{ "additionalOptions" | i18n }}

+
+ + + + {{ "notes" | i18n }} + + + + + {{ "passwordPrompt" | i18n }} + + + + +
+ + diff --git a/libs/vault/src/cipher-form/components/additional-options/additional-options-section.component.spec.ts b/libs/vault/src/cipher-form/components/additional-options/additional-options-section.component.spec.ts new file mode 100644 index 00000000000..71f8c4f197b --- /dev/null +++ b/libs/vault/src/cipher-form/components/additional-options/additional-options-section.component.spec.ts @@ -0,0 +1,99 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { mock, MockProxy } from "jest-mock-extended"; +import { BehaviorSubject } from "rxjs"; + +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; + +import { PasswordRepromptService } from "../../../services/password-reprompt.service"; +import { CipherFormContainer } from "../../cipher-form-container"; + +import { AdditionalOptionsSectionComponent } from "./additional-options-section.component"; + +describe("AdditionalOptionsSectionComponent", () => { + let component: AdditionalOptionsSectionComponent; + let fixture: ComponentFixture; + let cipherFormProvider: MockProxy; + let passwordRepromptService: MockProxy; + let passwordRepromptEnabled$: BehaviorSubject; + + beforeEach(async () => { + cipherFormProvider = mock(); + + passwordRepromptService = mock(); + passwordRepromptEnabled$ = new BehaviorSubject(true); + passwordRepromptService.enabled$ = passwordRepromptEnabled$; + + await TestBed.configureTestingModule({ + imports: [AdditionalOptionsSectionComponent], + providers: [ + { provide: CipherFormContainer, useValue: cipherFormProvider }, + { provide: PasswordRepromptService, useValue: passwordRepromptService }, + { provide: I18nService, useValue: mock() }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(AdditionalOptionsSectionComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it("registers 'additionalOptionsForm' form with CipherFormContainer", () => { + expect(cipherFormProvider.registerChildForm).toHaveBeenCalledWith( + "additionalOptions", + component.additionalOptionsForm, + ); + }); + + it("patches 'additionalOptionsForm' changes to CipherFormContainer", () => { + component.additionalOptionsForm.patchValue({ + notes: "new notes", + reprompt: true, + }); + + expect(cipherFormProvider.patchCipher).toHaveBeenCalledWith({ + notes: "new notes", + reprompt: 1, + }); + }); + + it("disables 'additionalOptionsForm' when in partial-edit mode", () => { + cipherFormProvider.config.mode = "partial-edit"; + + component.ngOnInit(); + + expect(component.additionalOptionsForm.disabled).toBe(true); + }); + + it("initializes 'additionalOptionsForm' with original cipher view values", () => { + (cipherFormProvider.originalCipherView as any) = { + notes: "original notes", + reprompt: 1, + } as CipherView; + + component.ngOnInit(); + + expect(component.additionalOptionsForm.value).toEqual({ + notes: "original notes", + reprompt: true, + }); + }); + + it("hides password reprompt checkbox when disabled", () => { + passwordRepromptEnabled$.next(true); + fixture.detectChanges(); + + let checkbox = fixture.nativeElement.querySelector("input[formControlName='reprompt']"); + expect(checkbox).not.toBeNull(); + + passwordRepromptEnabled$.next(false); + fixture.detectChanges(); + + checkbox = fixture.nativeElement.querySelector("input[formControlName='reprompt']"); + expect(checkbox).toBeNull(); + }); +}); diff --git a/libs/vault/src/cipher-form/components/additional-options/additional-options-section.component.ts b/libs/vault/src/cipher-form/components/additional-options/additional-options-section.component.ts new file mode 100644 index 00000000000..9cd1c2ac5cd --- /dev/null +++ b/libs/vault/src/cipher-form/components/additional-options/additional-options-section.component.ts @@ -0,0 +1,75 @@ +import { CommonModule } from "@angular/common"; +import { Component, OnInit } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { FormBuilder, ReactiveFormsModule } from "@angular/forms"; +import { shareReplay } from "rxjs"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { CipherRepromptType } from "@bitwarden/common/vault/enums"; +import { + CardComponent, + CheckboxModule, + FormFieldModule, + SectionComponent, + SectionHeaderComponent, + TypographyModule, +} from "@bitwarden/components"; + +import { PasswordRepromptService } from "../../../services/password-reprompt.service"; +import { CipherFormContainer } from "../../cipher-form-container"; + +@Component({ + selector: "vault-additional-options-section", + templateUrl: "./additional-options-section.component.html", + standalone: true, + imports: [ + SectionComponent, + SectionHeaderComponent, + TypographyModule, + JslibModule, + CardComponent, + FormFieldModule, + ReactiveFormsModule, + CheckboxModule, + CommonModule, + ], +}) +export class AdditionalOptionsSectionComponent implements OnInit { + additionalOptionsForm = this.formBuilder.group({ + notes: [null as string], + reprompt: [false], + }); + + passwordRepromptEnabled$ = this.passwordRepromptService.enabled$.pipe( + shareReplay({ refCount: false, bufferSize: 1 }), + ); + + constructor( + private cipherFormContainer: CipherFormContainer, + private formBuilder: FormBuilder, + private passwordRepromptService: PasswordRepromptService, + ) { + this.cipherFormContainer.registerChildForm("additionalOptions", this.additionalOptionsForm); + + this.additionalOptionsForm.valueChanges.pipe(takeUntilDestroyed()).subscribe((value) => { + this.cipherFormContainer.patchCipher({ + notes: value.notes, + reprompt: value.reprompt ? CipherRepromptType.Password : CipherRepromptType.None, + }); + }); + } + + ngOnInit() { + if (this.cipherFormContainer.originalCipherView) { + this.additionalOptionsForm.patchValue({ + notes: this.cipherFormContainer.originalCipherView.notes, + reprompt: + this.cipherFormContainer.originalCipherView.reprompt === CipherRepromptType.Password, + }); + } + + if (this.cipherFormContainer.config.mode === "partial-edit") { + this.additionalOptionsForm.disable(); + } + } +} diff --git a/libs/vault/src/cipher-form/components/cipher-form.component.html b/libs/vault/src/cipher-form/components/cipher-form.component.html index 8b5d4708997..669f3c8b963 100644 --- a/libs/vault/src/cipher-form/components/cipher-form.component.html +++ b/libs/vault/src/cipher-form/components/cipher-form.component.html @@ -18,6 +18,8 @@ [disabled]="config.mode === 'partial-edit'" > + + diff --git a/libs/vault/src/cipher-form/components/cipher-form.component.ts b/libs/vault/src/cipher-form/components/cipher-form.component.ts index 3307425e662..9d5e0684d2d 100644 --- a/libs/vault/src/cipher-form/components/cipher-form.component.ts +++ b/libs/vault/src/cipher-form/components/cipher-form.component.ts @@ -35,6 +35,7 @@ import { CipherFormConfig } from "../abstractions/cipher-form-config.service"; import { CipherFormService } from "../abstractions/cipher-form.service"; import { CipherForm, CipherFormContainer } from "../cipher-form-container"; +import { AdditionalOptionsSectionComponent } from "./additional-options/additional-options-section.component"; import { CardDetailsSectionComponent } from "./card-details-section/card-details-section.component"; import { IdentitySectionComponent } from "./identity/identity.component"; import { ItemDetailsSectionComponent } from "./item-details/item-details-section.component"; @@ -62,6 +63,7 @@ import { ItemDetailsSectionComponent } from "./item-details/item-details-section CardDetailsSectionComponent, IdentitySectionComponent, NgIf, + AdditionalOptionsSectionComponent, ], }) export class CipherFormComponent implements AfterViewInit, OnInit, OnChanges, CipherFormContainer { @@ -91,18 +93,17 @@ export class CipherFormComponent implements AfterViewInit, OnInit, OnChanges, Ci */ @Output() cipherSaved = new EventEmitter(); + /** + * The original cipher being edited or cloned. Null for add mode. + */ + originalCipherView: CipherView | null; + /** * The form group for the cipher. Starts empty and is populated by child components via the `registerChildForm` method. * @protected */ protected cipherForm = this.formBuilder.group({}); - /** - * The original cipher being edited or cloned. Null for add mode. - * @protected - */ - protected originalCipherView: CipherView | null; - /** * The value of the updated cipher. Starts as a new cipher (or clone of originalCipher) and is updated * by child components via the `patchCipher` method. diff --git a/libs/vault/src/services/password-reprompt.service.ts b/libs/vault/src/services/password-reprompt.service.ts index 8621c436bae..6583d0787fc 100644 --- a/libs/vault/src/services/password-reprompt.service.ts +++ b/libs/vault/src/services/password-reprompt.service.ts @@ -1,7 +1,8 @@ import { Injectable } from "@angular/core"; -import { lastValueFrom } from "rxjs"; +import { firstValueFrom, lastValueFrom } from "rxjs"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; import { CipherRepromptType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { DialogService } from "@bitwarden/components"; @@ -19,6 +20,10 @@ export class PasswordRepromptService { private userVerificationService: UserVerificationService, ) {} + enabled$ = Utils.asyncToObservable(() => + this.userVerificationService.hasMasterPasswordAndMasterKeyHash(), + ); + protectedFields() { return ["TOTP", "Password", "H_Field", "Card Number", "Security Code"]; } @@ -45,7 +50,7 @@ export class PasswordRepromptService { return result === true; } - async enabled() { - return await this.userVerificationService.hasMasterPasswordAndMasterKeyHash(); + enabled() { + return firstValueFrom(this.enabled$); } } From 66f432d6e034af1227507147cf0cbc3e4456878d Mon Sep 17 00:00:00 2001 From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> Date: Thu, 11 Jul 2024 17:32:53 -0500 Subject: [PATCH 04/57] add fallback for navigating the user back when the browser is in a popout (#10024) --- .../add-edit/add-edit-v2.component.html | 9 ++++++-- .../add-edit/add-edit-v2.component.ts | 23 ++++++++++++++++--- .../attachments/attachments-v2.component.html | 7 +++++- .../attachments-v2.component.spec.ts | 1 + .../attachments/attachments-v2.component.ts | 18 ++++++++++++++- 5 files changed, 51 insertions(+), 7 deletions(-) diff --git a/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.html b/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.html index 4d8461a57c3..0ae2f0af01f 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.html +++ b/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.html @@ -1,11 +1,16 @@ - + - + diff --git a/apps/browser/src/vault/popup/components/vault-v2/attachments/attachments-v2.component.spec.ts b/apps/browser/src/vault/popup/components/vault-v2/attachments/attachments-v2.component.spec.ts index 0f09d12db9f..342042c95f3 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/attachments/attachments-v2.component.spec.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/attachments/attachments-v2.component.spec.ts @@ -26,6 +26,7 @@ import { CipherAttachmentsComponent } from "./cipher-attachments/cipher-attachme }) class MockPopupHeaderComponent { @Input() pageTitle: string; + @Input() backAction: () => void; } @Component({ diff --git a/apps/browser/src/vault/popup/components/vault-v2/attachments/attachments-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/attachments/attachments-v2.component.ts index da0def529c2..20e553ca748 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/attachments/attachments-v2.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/attachments/attachments-v2.component.ts @@ -1,4 +1,4 @@ -import { CommonModule } from "@angular/common"; +import { CommonModule, Location } from "@angular/common"; import { Component } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { ActivatedRoute, Router } from "@angular/router"; @@ -41,6 +41,7 @@ export class AttachmentsV2Component { constructor( private router: Router, private cipherService: CipherService, + private location: Location, route: ActivatedRoute, ) { route.queryParams.pipe(takeUntilDestroyed(), first()).subscribe(({ cipherId }) => { @@ -48,6 +49,21 @@ export class AttachmentsV2Component { }); } + /** + * Navigates to previous view or edit-cipher path + * depending on the history length. + * + * This can happen when history is lost due to the extension being + * forced into a popout window. + */ + async handleBackButton() { + if (history.length === 1) { + await this.navigateToEditScreen(); + } else { + this.location.back(); + } + } + /** Navigate the user back to the edit screen after uploading an attachment */ async navigateToEditScreen() { const cipherDomain = await this.cipherService.get(this.cipherId); From d7fa592521c50241817c1908c222e843fd1c8dbe Mon Sep 17 00:00:00 2001 From: Jason Ng Date: Thu, 11 Jul 2024 18:47:54 -0400 Subject: [PATCH 05/57] [PM-9646] Identity v2 Edit Defects (#10076) * add necessary module to identity. update toggle btn with testing attr --- .../components/identity/identity.component.html | 16 ++++++++++++++-- .../components/identity/identity.component.ts | 2 ++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/libs/vault/src/cipher-form/components/identity/identity.component.html b/libs/vault/src/cipher-form/components/identity/identity.component.html index f55397ce464..09abfb6b157 100644 --- a/libs/vault/src/cipher-form/components/identity/identity.component.html +++ b/libs/vault/src/cipher-form/components/identity/identity.component.html @@ -53,14 +53,26 @@ {{ "ssn" | i18n }} - + {{ "passportNumber" | i18n }} - + diff --git a/libs/vault/src/cipher-form/components/identity/identity.component.ts b/libs/vault/src/cipher-form/components/identity/identity.component.ts index c1f44503610..a5545815e30 100644 --- a/libs/vault/src/cipher-form/components/identity/identity.component.ts +++ b/libs/vault/src/cipher-form/components/identity/identity.component.ts @@ -15,6 +15,7 @@ import { FormFieldModule, IconButtonModule, SelectModule, + TypographyModule, } from "@bitwarden/components"; import { CipherFormContainer } from "../../cipher-form-container"; @@ -34,6 +35,7 @@ import { CipherFormContainer } from "../../cipher-form-container"; FormFieldModule, IconButtonModule, SelectModule, + TypographyModule, ], }) export class IdentitySectionComponent implements OnInit { From 7243d62b19c447f2a51f372bb988ca951ac453e1 Mon Sep 17 00:00:00 2001 From: Jason Ng Date: Thu, 11 Jul 2024 18:48:47 -0400 Subject: [PATCH 06/57] [PM-9627] item details v2 defects (#10082) * Pull proper item name in item details. add Typography Module to reusable view sections --- .../additional-information.component.ts | 2 ++ .../attachments/attachments-v2.component.ts | 2 ++ .../custom-fields-v2.component.ts | 2 ++ .../item-details-v2.component.html | 27 +++++++++++-------- .../item-details/item-details-v2.component.ts | 16 +++++++++-- .../item-history/item-history-v2.component.ts | 8 +++++- 6 files changed, 43 insertions(+), 14 deletions(-) diff --git a/libs/vault/src/cipher-view/additional-information/additional-information.component.ts b/libs/vault/src/cipher-view/additional-information/additional-information.component.ts index a9660f3fc27..9e1376a8066 100644 --- a/libs/vault/src/cipher-view/additional-information/additional-information.component.ts +++ b/libs/vault/src/cipher-view/additional-information/additional-information.component.ts @@ -8,6 +8,7 @@ import { InputModule, SectionComponent, SectionHeaderComponent, + TypographyModule, } from "@bitwarden/components"; @Component({ @@ -22,6 +23,7 @@ import { InputModule, SectionComponent, SectionHeaderComponent, + TypographyModule, ], }) export class AdditionalInformationComponent { diff --git a/libs/vault/src/cipher-view/attachments/attachments-v2.component.ts b/libs/vault/src/cipher-view/attachments/attachments-v2.component.ts index c274fa4e9ac..6ea96bec497 100644 --- a/libs/vault/src/cipher-view/attachments/attachments-v2.component.ts +++ b/libs/vault/src/cipher-view/attachments/attachments-v2.component.ts @@ -22,6 +22,7 @@ import { IconButtonModule, SectionComponent, SectionHeaderComponent, + TypographyModule, } from "@bitwarden/components"; import { PasswordRepromptService } from "@bitwarden/vault"; @@ -36,6 +37,7 @@ import { PasswordRepromptService } from "@bitwarden/vault"; IconButtonModule, SectionComponent, SectionHeaderComponent, + TypographyModule, ], }) export class AttachmentsV2Component { diff --git a/libs/vault/src/cipher-view/custom-fields/custom-fields-v2.component.ts b/libs/vault/src/cipher-view/custom-fields/custom-fields-v2.component.ts index a40bca2d261..e54e5996eb5 100644 --- a/libs/vault/src/cipher-view/custom-fields/custom-fields-v2.component.ts +++ b/libs/vault/src/cipher-view/custom-fields/custom-fields-v2.component.ts @@ -12,6 +12,7 @@ import { InputModule, SectionComponent, SectionHeaderComponent, + TypographyModule, } from "@bitwarden/components"; @Component({ @@ -27,6 +28,7 @@ import { InputModule, SectionComponent, SectionHeaderComponent, + TypographyModule, ], }) export class CustomFieldV2Component { diff --git a/libs/vault/src/cipher-view/item-details/item-details-v2.component.html b/libs/vault/src/cipher-view/item-details/item-details-v2.component.html index 0ade00679af..6941c25c2e3 100644 --- a/libs/vault/src/cipher-view/item-details/item-details-v2.component.html +++ b/libs/vault/src/cipher-view/item-details/item-details-v2.component.html @@ -7,27 +7,32 @@ - +
-
+

{{ "ownerYou" | i18n }} -

-
+

{{ organization.name }} -

-
-

+

+
    +

    {{ collection.name }} -

-
-
+

+ +

{{ folder.name }} -

+

diff --git a/libs/vault/src/cipher-view/item-details/item-details-v2.component.ts b/libs/vault/src/cipher-view/item-details/item-details-v2.component.ts index b0d158c1409..f4f60dc6f54 100644 --- a/libs/vault/src/cipher-view/item-details/item-details-v2.component.ts +++ b/libs/vault/src/cipher-view/item-details/item-details-v2.component.ts @@ -6,13 +6,25 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; -import { CardComponent, SectionComponent, SectionHeaderComponent } from "@bitwarden/components"; +import { + CardComponent, + SectionComponent, + SectionHeaderComponent, + TypographyModule, +} from "@bitwarden/components"; @Component({ selector: "app-item-details-v2", templateUrl: "item-details-v2.component.html", standalone: true, - imports: [CommonModule, JslibModule, CardComponent, SectionComponent, SectionHeaderComponent], + imports: [ + CommonModule, + JslibModule, + CardComponent, + SectionComponent, + SectionHeaderComponent, + TypographyModule, + ], }) export class ItemDetailsV2Component { @Input() cipher: CipherView; diff --git a/libs/vault/src/cipher-view/item-history/item-history-v2.component.ts b/libs/vault/src/cipher-view/item-history/item-history-v2.component.ts index 51badfdbc89..830e37da61e 100644 --- a/libs/vault/src/cipher-view/item-history/item-history-v2.component.ts +++ b/libs/vault/src/cipher-view/item-history/item-history-v2.component.ts @@ -4,7 +4,12 @@ import { RouterModule } from "@angular/router"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; -import { CardComponent, SectionComponent, SectionHeaderComponent } from "@bitwarden/components"; +import { + CardComponent, + SectionComponent, + SectionHeaderComponent, + TypographyModule, +} from "@bitwarden/components"; @Component({ selector: "app-item-history-v2", @@ -17,6 +22,7 @@ import { CardComponent, SectionComponent, SectionHeaderComponent } from "@bitwar CardComponent, SectionComponent, SectionHeaderComponent, + TypographyModule, ], }) export class ItemHistoryV2Component { From fef188c221fcd28c2332a590a8c85b3e2aae35bc Mon Sep 17 00:00:00 2001 From: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com> Date: Thu, 11 Jul 2024 20:03:37 -0400 Subject: [PATCH 07/57] PM-9688 - Registration with Email Verification - RegistrationFinishComponent - fix button text to say create account instead of set password (#10084) --- .../registration-finish/registration-finish.component.html | 1 + 1 file changed, 1 insertion(+) diff --git a/libs/auth/src/angular/registration/registration-finish/registration-finish.component.html b/libs/auth/src/angular/registration/registration-finish/registration-finish.component.html index 217a7745ebf..70ca948f93d 100644 --- a/libs/auth/src/angular/registration/registration-finish/registration-finish.component.html +++ b/libs/auth/src/angular/registration/registration-finish/registration-finish.component.html @@ -8,4 +8,5 @@ [masterPasswordPolicyOptions]="masterPasswordPolicyOptions" [loading]="submitting" (onPasswordFormSubmit)="handlePasswordFormSubmit($event)" + [buttonText]="'createAccount' | i18n" > From 36aacb6687b24260bbf7b9fc82e2eb78903cefb2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 12 Jul 2024 09:29:21 +0000 Subject: [PATCH 08/57] Autosync the updated translations (#10087) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/desktop/src/locales/af/messages.json | 15 ++++++ apps/desktop/src/locales/ar/messages.json | 15 ++++++ apps/desktop/src/locales/az/messages.json | 15 ++++++ apps/desktop/src/locales/be/messages.json | 15 ++++++ apps/desktop/src/locales/bg/messages.json | 15 ++++++ apps/desktop/src/locales/bn/messages.json | 15 ++++++ apps/desktop/src/locales/bs/messages.json | 15 ++++++ apps/desktop/src/locales/ca/messages.json | 15 ++++++ apps/desktop/src/locales/cs/messages.json | 15 ++++++ apps/desktop/src/locales/cy/messages.json | 15 ++++++ apps/desktop/src/locales/da/messages.json | 15 ++++++ apps/desktop/src/locales/de/messages.json | 31 +++++++++--- apps/desktop/src/locales/el/messages.json | 15 ++++++ apps/desktop/src/locales/en_GB/messages.json | 15 ++++++ apps/desktop/src/locales/en_IN/messages.json | 15 ++++++ apps/desktop/src/locales/eo/messages.json | 15 ++++++ apps/desktop/src/locales/es/messages.json | 15 ++++++ apps/desktop/src/locales/et/messages.json | 15 ++++++ apps/desktop/src/locales/eu/messages.json | 15 ++++++ apps/desktop/src/locales/fa/messages.json | 15 ++++++ apps/desktop/src/locales/fi/messages.json | 15 ++++++ apps/desktop/src/locales/fil/messages.json | 15 ++++++ apps/desktop/src/locales/fr/messages.json | 35 +++++++++---- apps/desktop/src/locales/gl/messages.json | 15 ++++++ apps/desktop/src/locales/he/messages.json | 15 ++++++ apps/desktop/src/locales/hi/messages.json | 15 ++++++ apps/desktop/src/locales/hr/messages.json | 15 ++++++ apps/desktop/src/locales/hu/messages.json | 15 ++++++ apps/desktop/src/locales/id/messages.json | 15 ++++++ apps/desktop/src/locales/it/messages.json | 15 ++++++ apps/desktop/src/locales/ja/messages.json | 15 ++++++ apps/desktop/src/locales/ka/messages.json | 15 ++++++ apps/desktop/src/locales/km/messages.json | 15 ++++++ apps/desktop/src/locales/kn/messages.json | 15 ++++++ apps/desktop/src/locales/ko/messages.json | 15 ++++++ apps/desktop/src/locales/lt/messages.json | 15 ++++++ apps/desktop/src/locales/lv/messages.json | 15 ++++++ apps/desktop/src/locales/me/messages.json | 15 ++++++ apps/desktop/src/locales/ml/messages.json | 15 ++++++ apps/desktop/src/locales/mr/messages.json | 15 ++++++ apps/desktop/src/locales/my/messages.json | 15 ++++++ apps/desktop/src/locales/nb/messages.json | 15 ++++++ apps/desktop/src/locales/ne/messages.json | 15 ++++++ apps/desktop/src/locales/nl/messages.json | 15 ++++++ apps/desktop/src/locales/nn/messages.json | 15 ++++++ apps/desktop/src/locales/or/messages.json | 15 ++++++ apps/desktop/src/locales/pl/messages.json | 15 ++++++ apps/desktop/src/locales/pt_BR/messages.json | 15 ++++++ apps/desktop/src/locales/pt_PT/messages.json | 21 ++++++-- apps/desktop/src/locales/ro/messages.json | 15 ++++++ apps/desktop/src/locales/ru/messages.json | 15 ++++++ apps/desktop/src/locales/si/messages.json | 15 ++++++ apps/desktop/src/locales/sk/messages.json | 15 ++++++ apps/desktop/src/locales/sl/messages.json | 15 ++++++ apps/desktop/src/locales/sr/messages.json | 15 ++++++ apps/desktop/src/locales/sv/messages.json | 15 ++++++ apps/desktop/src/locales/te/messages.json | 15 ++++++ apps/desktop/src/locales/th/messages.json | 15 ++++++ apps/desktop/src/locales/tr/messages.json | 53 +++++++++++++------- apps/desktop/src/locales/uk/messages.json | 15 ++++++ apps/desktop/src/locales/vi/messages.json | 15 ++++++ apps/desktop/src/locales/zh_CN/messages.json | 15 ++++++ apps/desktop/src/locales/zh_TW/messages.json | 15 ++++++ 63 files changed, 985 insertions(+), 40 deletions(-) diff --git a/apps/desktop/src/locales/af/messages.json b/apps/desktop/src/locales/af/messages.json index 3e643925e7a..84a9913c287 100644 --- a/apps/desktop/src/locales/af/messages.json +++ b/apps/desktop/src/locales/af/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "settings": { "message": "Instellings" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "Vereis e-posbevestiging" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "U moet u e-pos bevestig om die funksie te gebruik." }, diff --git a/apps/desktop/src/locales/ar/messages.json b/apps/desktop/src/locales/ar/messages.json index b80ec1b4dc9..5885f86f253 100644 --- a/apps/desktop/src/locales/ar/messages.json +++ b/apps/desktop/src/locales/ar/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "settings": { "message": "الإعدادات" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "التحقق من البريد الإلكتروني مطلوب" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "يجب عليك التحقق من بريدك الإلكتروني لاستخدام هذه الميزة." }, diff --git a/apps/desktop/src/locales/az/messages.json b/apps/desktop/src/locales/az/messages.json index 3481c03dfaa..b9dfef03424 100644 --- a/apps/desktop/src/locales/az/messages.json +++ b/apps/desktop/src/locales/az/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Ana parol" + }, + "masterPassImportant": { + "message": "Unutsanız, ana parolunuz geri qaytarıla bilməz!" + }, + "confirmMasterPassword": { + "message": "Ana parolu təsdiqlə" + }, + "masterPassHintLabel": { + "message": "Ana parol ipucusu" + }, "settings": { "message": "Ayarlar" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "E-poçtun doğrulanması tələb olunur" }, + "emailVerifiedV2": { + "message": "E-poçt doğrulandı" + }, "emailVerificationRequiredDesc": { "message": "Bu özəlliyi istifadə etmək üçün e-poçtunuzu doğrulamalısınız." }, diff --git a/apps/desktop/src/locales/be/messages.json b/apps/desktop/src/locales/be/messages.json index c1fa1119706..e729d9ca0a2 100644 --- a/apps/desktop/src/locales/be/messages.json +++ b/apps/desktop/src/locales/be/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "settings": { "message": "Налады" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "Патрабуецца праверка электроннай пошты" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "Для выкарыстання гэтай функцыі патрабуецца праверыць электронную пошту." }, diff --git a/apps/desktop/src/locales/bg/messages.json b/apps/desktop/src/locales/bg/messages.json index 23657992a5e..41ff64598e9 100644 --- a/apps/desktop/src/locales/bg/messages.json +++ b/apps/desktop/src/locales/bg/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Главна парола" + }, + "masterPassImportant": { + "message": "Главната парола не може да бъде възстановена, ако я забравите!" + }, + "confirmMasterPassword": { + "message": "Потвърждаване на главната парола" + }, + "masterPassHintLabel": { + "message": "Подсказка за главната парола" + }, "settings": { "message": "Настройки" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "Изисква се потвърждение на е-пощата" }, + "emailVerifiedV2": { + "message": "Е-пощата е потвърдена" + }, "emailVerificationRequiredDesc": { "message": "Трябва да потвърдите е-пощата си, за да можете да използвате тази функционалност." }, diff --git a/apps/desktop/src/locales/bn/messages.json b/apps/desktop/src/locales/bn/messages.json index a383dd0a328..dd173da327f 100644 --- a/apps/desktop/src/locales/bn/messages.json +++ b/apps/desktop/src/locales/bn/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "settings": { "message": "সেটিংস" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature." }, diff --git a/apps/desktop/src/locales/bs/messages.json b/apps/desktop/src/locales/bs/messages.json index a3e5c4dc2c4..72ad7db55de 100644 --- a/apps/desktop/src/locales/bs/messages.json +++ b/apps/desktop/src/locales/bs/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "settings": { "message": "Postavke" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature." }, diff --git a/apps/desktop/src/locales/ca/messages.json b/apps/desktop/src/locales/ca/messages.json index 41ab3bba0a4..1d733b87a95 100644 --- a/apps/desktop/src/locales/ca/messages.json +++ b/apps/desktop/src/locales/ca/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "settings": { "message": "Configuració" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "Es requereix verificació per correu electrònic" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "Heu de verificar el vostre correu electrònic per utilitzar aquesta característica." }, diff --git a/apps/desktop/src/locales/cs/messages.json b/apps/desktop/src/locales/cs/messages.json index dee3d75f694..59535060372 100644 --- a/apps/desktop/src/locales/cs/messages.json +++ b/apps/desktop/src/locales/cs/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Hlavní heslo" + }, + "masterPassImportant": { + "message": "Pokud zapomenete Vaše hlavní heslo, nebude možné jej obnovit!" + }, + "confirmMasterPassword": { + "message": "Potvrzení hlavního hesla" + }, + "masterPassHintLabel": { + "message": "Nápověda k hlavnímu heslu" + }, "settings": { "message": "Nastavení" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "Je vyžadováno ověření e-mailu" }, + "emailVerifiedV2": { + "message": "E-mail byl ověřen" + }, "emailVerificationRequiredDesc": { "message": "Pro použití této funkce musíte ověřit svůj e-mail." }, diff --git a/apps/desktop/src/locales/cy/messages.json b/apps/desktop/src/locales/cy/messages.json index 94f20c3e300..8d6988f73e3 100644 --- a/apps/desktop/src/locales/cy/messages.json +++ b/apps/desktop/src/locales/cy/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "settings": { "message": "Settings" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature." }, diff --git a/apps/desktop/src/locales/da/messages.json b/apps/desktop/src/locales/da/messages.json index bdb296ba717..8c20ec2c4a2 100644 --- a/apps/desktop/src/locales/da/messages.json +++ b/apps/desktop/src/locales/da/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Hovedadgangskode" + }, + "masterPassImportant": { + "message": "Hovedadgangskoden kan ikke gendannes, hvis den glemmes!" + }, + "confirmMasterPassword": { + "message": "Bekræft hovedadgangskode" + }, + "masterPassHintLabel": { + "message": "Hovedadgangskodetip" + }, "settings": { "message": "Indstillinger" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "E-mailbekræftelse kræves" }, + "emailVerifiedV2": { + "message": "E-mail bekræftet" + }, "emailVerificationRequiredDesc": { "message": "Du skal bekræfte din mailadresse for at bruge denne funktion." }, diff --git a/apps/desktop/src/locales/de/messages.json b/apps/desktop/src/locales/de/messages.json index ffd018bcc72..c52ff1d833a 100644 --- a/apps/desktop/src/locales/de/messages.json +++ b/apps/desktop/src/locales/de/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Master-Passwort" + }, + "masterPassImportant": { + "message": "Dein Master-Passwort kann nicht wiederhergestellt werden, wenn du es vergisst!" + }, + "confirmMasterPassword": { + "message": "Master-Passwort bestätigen" + }, + "masterPassHintLabel": { + "message": "Master-Passwort-Hinweis" + }, "settings": { "message": "Einstellungen" }, @@ -667,17 +679,17 @@ "message": "Authenticator App" }, "authenticatorAppDescV2": { - "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "message": "Gib einen Code ein, der von einer Authentifizierungs-App wie dem Bitwarden Authenticator generiert wurde.", "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, "yubiKeyTitleV2": { - "message": "Yubico OTP security key" + "message": "Yubico OTP-Sicherheitsschlüssel" }, "yubiKeyDesc": { "message": "Verwende einen YubiKey, um auf dein Konto zuzugreifen. Funktioniert mit den Geräten YubiKey 4, Nano 4, 4C und NEO." }, "duoDescV2": { - "message": "Enter a code generated by Duo Security.", + "message": "Gib einen von Duo Security generierten Code ein.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -694,7 +706,7 @@ "message": "E-Mail" }, "emailDescV2": { - "message": "Enter a code sent to your email." + "message": "Gib einen an deine E-Mail-Adresse gesendeten Code ein." }, "loginUnavailable": { "message": "Anmeldung nicht verfügbar" @@ -1685,13 +1697,13 @@ "message": "Deabonnieren" }, "atAnyTime": { - "message": "at any time." + "message": "jederzeit." }, "byContinuingYouAgreeToThe": { - "message": "By continuing, you agree to the" + "message": "Indem du fortfährst, stimmst du den" }, "and": { - "message": "and" + "message": "und" }, "acceptPolicies": { "message": "Durch Anwählen dieses Kästchens erklärst du dich mit folgendem einverstanden:" @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "E-Mail-Verifizierung erforderlich" }, + "emailVerifiedV2": { + "message": "E-Mail-Adresse verifiziert" + }, "emailVerificationRequiredDesc": { "message": "Du musst deine E-Mail verifizieren, um diese Funktion nutzen zu können." }, @@ -2844,7 +2859,7 @@ "message": "Dateipasswort bestätigen" }, "exportSuccess": { - "message": "Vault data exported" + "message": "Tresor-Daten exportiert" }, "multifactorAuthenticationCancelled": { "message": "Multifaktor-Authentifizierung abgebrochen" diff --git a/apps/desktop/src/locales/el/messages.json b/apps/desktop/src/locales/el/messages.json index 911a3735e6d..f1f950e3c22 100644 --- a/apps/desktop/src/locales/el/messages.json +++ b/apps/desktop/src/locales/el/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "settings": { "message": "Ρυθμίσεις" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "Απαιτείται Επαλήθευση Email" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "Πρέπει να επαληθεύσετε το email σας για να χρησιμοποιήσετε αυτή τη δυνατότητα." }, diff --git a/apps/desktop/src/locales/en_GB/messages.json b/apps/desktop/src/locales/en_GB/messages.json index 63d9cafd136..9c7aea781d4 100644 --- a/apps/desktop/src/locales/en_GB/messages.json +++ b/apps/desktop/src/locales/en_GB/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "settings": { "message": "Settings" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature." }, diff --git a/apps/desktop/src/locales/en_IN/messages.json b/apps/desktop/src/locales/en_IN/messages.json index 6fa8989e863..5e404b77f09 100644 --- a/apps/desktop/src/locales/en_IN/messages.json +++ b/apps/desktop/src/locales/en_IN/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "settings": { "message": "Settings" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "Email Verification Required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature." }, diff --git a/apps/desktop/src/locales/eo/messages.json b/apps/desktop/src/locales/eo/messages.json index cbea97186fd..8bde0fc3c9b 100644 --- a/apps/desktop/src/locales/eo/messages.json +++ b/apps/desktop/src/locales/eo/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "settings": { "message": "Settings" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature." }, diff --git a/apps/desktop/src/locales/es/messages.json b/apps/desktop/src/locales/es/messages.json index a0fc028f830..f31fbe6ba82 100644 --- a/apps/desktop/src/locales/es/messages.json +++ b/apps/desktop/src/locales/es/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "settings": { "message": "Ajustes" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "Verificación de correo electrónico requerida" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "Debes verificar tu correo electrónico para usar esta característica." }, diff --git a/apps/desktop/src/locales/et/messages.json b/apps/desktop/src/locales/et/messages.json index e799f8a2f40..4c3ba957ea4 100644 --- a/apps/desktop/src/locales/et/messages.json +++ b/apps/desktop/src/locales/et/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "settings": { "message": "Seaded" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "Vajalik on e-posti kinnitamine" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "Enne selle funktsiooni kasutamist pead oma e-posti kinnitama." }, diff --git a/apps/desktop/src/locales/eu/messages.json b/apps/desktop/src/locales/eu/messages.json index e7347a53cf5..1ecfb02d902 100644 --- a/apps/desktop/src/locales/eu/messages.json +++ b/apps/desktop/src/locales/eu/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "settings": { "message": "Ezarpenak" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "Emailaren egiaztapena behar da" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "Emaila egiaztatu behar duzu funtzio hau erabiltzeko." }, diff --git a/apps/desktop/src/locales/fa/messages.json b/apps/desktop/src/locales/fa/messages.json index aac7d569cb1..29cde5e0cad 100644 --- a/apps/desktop/src/locales/fa/messages.json +++ b/apps/desktop/src/locales/fa/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "settings": { "message": "تنظیمات" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "تأیید ایمیل لازم است" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "برای استفاده از این ویژگی باید ایمیل خود را تأیید کنید." }, diff --git a/apps/desktop/src/locales/fi/messages.json b/apps/desktop/src/locales/fi/messages.json index f42aa7d127a..40a87ebc28c 100644 --- a/apps/desktop/src/locales/fi/messages.json +++ b/apps/desktop/src/locales/fi/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "settings": { "message": "Asetukset" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "Sähköpostiosoite on vahvistettava" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "Sinun on vahvistettava sähköpostiosoitteesi käyttääksesi tätä ominaisuutta." }, diff --git a/apps/desktop/src/locales/fil/messages.json b/apps/desktop/src/locales/fil/messages.json index 8eb371f8dcb..0d4d6e8c8ab 100644 --- a/apps/desktop/src/locales/fil/messages.json +++ b/apps/desktop/src/locales/fil/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "settings": { "message": "Mga Preperensya" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "Kailangan ang pag verify ng email" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "Kailangan mong i verify ang iyong email upang magamit ang tampok na ito." }, diff --git a/apps/desktop/src/locales/fr/messages.json b/apps/desktop/src/locales/fr/messages.json index 817f200ce1f..8bdbf19bfaa 100644 --- a/apps/desktop/src/locales/fr/messages.json +++ b/apps/desktop/src/locales/fr/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "settings": { "message": "Paramètres" }, @@ -667,17 +679,17 @@ "message": "Application d'authentification" }, "authenticatorAppDescV2": { - "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "message": "Entrez un code généré par une application d'authentification comme Bitwarden Authenticator.", "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, "yubiKeyTitleV2": { - "message": "Yubico OTP security key" + "message": "Clé de sécurité Yubico OTP" }, "yubiKeyDesc": { "message": "Utiliser une YubiKey pour accéder à votre compte. Fonctionne avec les appareils YubiKey 4, 4 Nano, 4C et NEO." }, "duoDescV2": { - "message": "Enter a code generated by Duo Security.", + "message": "Entrez un code généré par Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -694,7 +706,7 @@ "message": "Courriel" }, "emailDescV2": { - "message": "Enter a code sent to your email." + "message": "Entrez le code envoyé par courriel." }, "loginUnavailable": { "message": "Identifiant non disponible" @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "Vérification de courriel requise" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "Vous devez vérifier votre courriel pour utiliser cette fonctionnalité." }, @@ -2157,7 +2172,7 @@ "message": "Export du coffre-fort de l'organisation" }, "exportingOrganizationVaultDesc": { - "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "message": "Seul le coffre-fort de l'organisation associé à $ORGANIZATION$ sera exporté. Les coffres individuels ou d'autres organisations ne seront pas inclus.", "placeholders": { "organization": { "content": "$1", @@ -2230,7 +2245,7 @@ "message": "Générer un alias de courriel avec un service de transfert externe." }, "forwarderError": { - "message": "$SERVICENAME$ error: $ERRORMESSAGE$", + "message": "Erreur de $SERVICENAME$ : $ERRORMESSAGE$", "description": "Reports an error returned by a forwarding service to the user.", "placeholders": { "servicename": { @@ -2258,7 +2273,7 @@ } }, "forwaderInvalidToken": { - "message": "Invalid $SERVICENAME$ API token", + "message": "Jeton d'API de $SERVICENAME$ non valide", "description": "Displayed when the user's API token is empty or rejected by the forwarding service.", "placeholders": { "servicename": { @@ -2268,7 +2283,7 @@ } }, "forwaderInvalidTokenWithMessage": { - "message": "Invalid $SERVICENAME$ API token: $ERRORMESSAGE$", + "message": "Jeton d'API de $SERVICENAME$ non valide : $ERRORMESSAGE$", "description": "Displayed when the user's API token is rejected by the forwarding service with an error message.", "placeholders": { "servicename": { @@ -2282,7 +2297,7 @@ } }, "forwarderNoAccountId": { - "message": "Unable to obtain $SERVICENAME$ masked email account ID.", + "message": "Impossible d'obtenir l'ID de compte de messagerie masqué de $SERVICENAME$.", "description": "Displayed when the forwarding service fails to return an account ID.", "placeholders": { "servicename": { @@ -2302,7 +2317,7 @@ } }, "forwarderNoUrl": { - "message": "Invalid $SERVICENAME$ url.", + "message": "URL de $SERVICENAME$ non valide.", "description": "Displayed when the url of the forwarding service wasn't supplied.", "placeholders": { "servicename": { diff --git a/apps/desktop/src/locales/gl/messages.json b/apps/desktop/src/locales/gl/messages.json index 0887d769823..fa4dcc02b87 100644 --- a/apps/desktop/src/locales/gl/messages.json +++ b/apps/desktop/src/locales/gl/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "settings": { "message": "Settings" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature." }, diff --git a/apps/desktop/src/locales/he/messages.json b/apps/desktop/src/locales/he/messages.json index 8d82180f1de..ebad16e89c8 100644 --- a/apps/desktop/src/locales/he/messages.json +++ b/apps/desktop/src/locales/he/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "settings": { "message": "הגדרות" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "נדרש כתובת אימייל לאימות" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "נדרש אישור אימות בדוא\"ל כדי לאפשר שימוש בתכונה זו." }, diff --git a/apps/desktop/src/locales/hi/messages.json b/apps/desktop/src/locales/hi/messages.json index 78367ecd0dd..d894e487d30 100644 --- a/apps/desktop/src/locales/hi/messages.json +++ b/apps/desktop/src/locales/hi/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "settings": { "message": "Settings" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature." }, diff --git a/apps/desktop/src/locales/hr/messages.json b/apps/desktop/src/locales/hr/messages.json index 6d26e9c3c6b..67b671ed196 100644 --- a/apps/desktop/src/locales/hr/messages.json +++ b/apps/desktop/src/locales/hr/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "settings": { "message": "Postavke" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "Potrebna je potvrda e-pošte" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "Za korištenje ove značajke, potrebna je ovjera e-pošte." }, diff --git a/apps/desktop/src/locales/hu/messages.json b/apps/desktop/src/locales/hu/messages.json index 14c97b81fe5..3ffdd7ef1e6 100644 --- a/apps/desktop/src/locales/hu/messages.json +++ b/apps/desktop/src/locales/hu/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Mesterjelszó" + }, + "masterPassImportant": { + "message": "A mesterjelszó nem állítható helyre, ha elfelejtik!" + }, + "confirmMasterPassword": { + "message": "Mesterjelszó megerősítése" + }, + "masterPassHintLabel": { + "message": "Mesterjelszó emlékeztető" + }, "settings": { "message": "Beállítások" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "Email hitelesítés szükséges" }, + "emailVerifiedV2": { + "message": "Az email cím ellenőrzésre került." + }, "emailVerificationRequiredDesc": { "message": "A funkció használatához ellenőrizni kell az email címet." }, diff --git a/apps/desktop/src/locales/id/messages.json b/apps/desktop/src/locales/id/messages.json index 574ab5bb6b2..6a45046ea59 100644 --- a/apps/desktop/src/locales/id/messages.json +++ b/apps/desktop/src/locales/id/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "settings": { "message": "Setelan" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "Verifikasi Email Diperlukan" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "Anda harus mengkonfirmasi email anda untuk menggunakan fitur ini." }, diff --git a/apps/desktop/src/locales/it/messages.json b/apps/desktop/src/locales/it/messages.json index c0c1e322c65..95131aa47dc 100644 --- a/apps/desktop/src/locales/it/messages.json +++ b/apps/desktop/src/locales/it/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "settings": { "message": "Impostazioni" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "Verifica email obbligatoria" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "Devi verificare la tua email per usare questa funzionalità." }, diff --git a/apps/desktop/src/locales/ja/messages.json b/apps/desktop/src/locales/ja/messages.json index 80060295411..bffd5de8396 100644 --- a/apps/desktop/src/locales/ja/messages.json +++ b/apps/desktop/src/locales/ja/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "マスターパスワード" + }, + "masterPassImportant": { + "message": "マスターパスワードを忘れた場合は復元できません!" + }, + "confirmMasterPassword": { + "message": "マスターパスワードの確認" + }, + "masterPassHintLabel": { + "message": "マスターパスワードのヒント" + }, "settings": { "message": "設定" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "メールアドレスの確認が必要です" }, + "emailVerifiedV2": { + "message": "メールアドレスを認証しました" + }, "emailVerificationRequiredDesc": { "message": "この機能を使用するにはメールアドレスを確認する必要があります。" }, diff --git a/apps/desktop/src/locales/ka/messages.json b/apps/desktop/src/locales/ka/messages.json index 0887d769823..fa4dcc02b87 100644 --- a/apps/desktop/src/locales/ka/messages.json +++ b/apps/desktop/src/locales/ka/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "settings": { "message": "Settings" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature." }, diff --git a/apps/desktop/src/locales/km/messages.json b/apps/desktop/src/locales/km/messages.json index 0887d769823..fa4dcc02b87 100644 --- a/apps/desktop/src/locales/km/messages.json +++ b/apps/desktop/src/locales/km/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "settings": { "message": "Settings" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature." }, diff --git a/apps/desktop/src/locales/kn/messages.json b/apps/desktop/src/locales/kn/messages.json index ae4d9150af5..9866dafb8fe 100644 --- a/apps/desktop/src/locales/kn/messages.json +++ b/apps/desktop/src/locales/kn/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "settings": { "message": "ಸೆಟ್ಟಿಂಗ್‍ಗಳು" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "ಇಮೇಲ್ ಪರಿಶೀಲನೆ ಅಗತ್ಯವಿದೆ" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "ಈ ವೈಶಿಷ್ಟ್ಯವನ್ನು ಬಳಸಲು ನಿಮ್ಮ ಇಮೇಲ್ ಅನ್ನು ನೀವು ಪರಿಶೀಲಿಸಬೇಕು." }, diff --git a/apps/desktop/src/locales/ko/messages.json b/apps/desktop/src/locales/ko/messages.json index 63d03f5ebf9..c2f3437f608 100644 --- a/apps/desktop/src/locales/ko/messages.json +++ b/apps/desktop/src/locales/ko/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "settings": { "message": "설정" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "이메일 인증 필요함" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "이 기능을 이용하기 위해서는 이메일을 인증해야 합니다." }, diff --git a/apps/desktop/src/locales/lt/messages.json b/apps/desktop/src/locales/lt/messages.json index e1020f7d237..77e9f40e25b 100644 --- a/apps/desktop/src/locales/lt/messages.json +++ b/apps/desktop/src/locales/lt/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "settings": { "message": "Nustatymai" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "Reikalingas elektroninio pašto patvirtinimas" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "Privalote patvirtinti savo el. paštą norint naudotis šia funkcija." }, diff --git a/apps/desktop/src/locales/lv/messages.json b/apps/desktop/src/locales/lv/messages.json index 64c27f9f627..fe6aad78be7 100644 --- a/apps/desktop/src/locales/lv/messages.json +++ b/apps/desktop/src/locales/lv/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Galvenā parole" + }, + "masterPassImportant": { + "message": "Galveno paroli nevar atgūt, ja tā tiek aizmirsta." + }, + "confirmMasterPassword": { + "message": "Apstiprināt galveno paroli" + }, + "masterPassHintLabel": { + "message": "Galvenās paroles norāde" + }, "settings": { "message": "Iestatījumi" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "Nepieciešama e-pasta adreses apstiprināšana" }, + "emailVerifiedV2": { + "message": "E-pasta adrese ir apliecināta" + }, "emailVerificationRequiredDesc": { "message": "Ir jāapstiprina e-pasta adrese, lai izmantotu šo iespēju." }, diff --git a/apps/desktop/src/locales/me/messages.json b/apps/desktop/src/locales/me/messages.json index 6e02878b12d..71e9fe25948 100644 --- a/apps/desktop/src/locales/me/messages.json +++ b/apps/desktop/src/locales/me/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "settings": { "message": "Podešavanja" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature." }, diff --git a/apps/desktop/src/locales/ml/messages.json b/apps/desktop/src/locales/ml/messages.json index 880cc5b0848..6bd7a323d73 100644 --- a/apps/desktop/src/locales/ml/messages.json +++ b/apps/desktop/src/locales/ml/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "settings": { "message": "ക്രമീകരണങ്ങള്‍" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature." }, diff --git a/apps/desktop/src/locales/mr/messages.json b/apps/desktop/src/locales/mr/messages.json index 0887d769823..fa4dcc02b87 100644 --- a/apps/desktop/src/locales/mr/messages.json +++ b/apps/desktop/src/locales/mr/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "settings": { "message": "Settings" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature." }, diff --git a/apps/desktop/src/locales/my/messages.json b/apps/desktop/src/locales/my/messages.json index d007a4e0f45..90632de6251 100644 --- a/apps/desktop/src/locales/my/messages.json +++ b/apps/desktop/src/locales/my/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "settings": { "message": "Settings" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature." }, diff --git a/apps/desktop/src/locales/nb/messages.json b/apps/desktop/src/locales/nb/messages.json index 03d6c95a293..e0f5fa0d532 100644 --- a/apps/desktop/src/locales/nb/messages.json +++ b/apps/desktop/src/locales/nb/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "settings": { "message": "Innstillinger" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "E-postbekreftelse kreves" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "Du må bekrefte E-postadressen din for å bruke denne funksjonen." }, diff --git a/apps/desktop/src/locales/ne/messages.json b/apps/desktop/src/locales/ne/messages.json index 560a5c41c70..a023d51c1b7 100644 --- a/apps/desktop/src/locales/ne/messages.json +++ b/apps/desktop/src/locales/ne/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "settings": { "message": "Settings" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature." }, diff --git a/apps/desktop/src/locales/nl/messages.json b/apps/desktop/src/locales/nl/messages.json index 796b23b0c36..3ea2be8da9b 100644 --- a/apps/desktop/src/locales/nl/messages.json +++ b/apps/desktop/src/locales/nl/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Hoofdwachtwoord" + }, + "masterPassImportant": { + "message": "Je kunt je hoofdwachtwoord niet herstellen als je het vergeet!" + }, + "confirmMasterPassword": { + "message": "Hoofdwachtwoord bevestigen" + }, + "masterPassHintLabel": { + "message": "Hoofdwachtwoordhint" + }, "settings": { "message": "Instellingen" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "E-mailverificatie vereist" }, + "emailVerifiedV2": { + "message": "E-mailadres geverifieerd" + }, "emailVerificationRequiredDesc": { "message": "Je moet je e-mailadres verifiëren om deze functionaliteit te gebruiken." }, diff --git a/apps/desktop/src/locales/nn/messages.json b/apps/desktop/src/locales/nn/messages.json index f6d4a6ed3c4..e8cea993671 100644 --- a/apps/desktop/src/locales/nn/messages.json +++ b/apps/desktop/src/locales/nn/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "settings": { "message": "Innstillingar" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature." }, diff --git a/apps/desktop/src/locales/or/messages.json b/apps/desktop/src/locales/or/messages.json index d6781321446..a939637ea8a 100644 --- a/apps/desktop/src/locales/or/messages.json +++ b/apps/desktop/src/locales/or/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "settings": { "message": "Settings" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature." }, diff --git a/apps/desktop/src/locales/pl/messages.json b/apps/desktop/src/locales/pl/messages.json index ead83d7fa7c..18f4c0bf0db 100644 --- a/apps/desktop/src/locales/pl/messages.json +++ b/apps/desktop/src/locales/pl/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Hasło główne" + }, + "masterPassImportant": { + "message": "Twoje hasło główne nie może zostać odzyskane, jeśli je zapomnisz!" + }, + "confirmMasterPassword": { + "message": "Potwierdź hasło główne" + }, + "masterPassHintLabel": { + "message": "Podpowiedź do hasła głównego" + }, "settings": { "message": "Ustawienia" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "Weryfikacja adresu e-mail jest wymagana" }, + "emailVerifiedV2": { + "message": "E-mail zweryfikowany" + }, "emailVerificationRequiredDesc": { "message": "Musisz zweryfikować adres e-mail, aby używać tej funkcji." }, diff --git a/apps/desktop/src/locales/pt_BR/messages.json b/apps/desktop/src/locales/pt_BR/messages.json index de5ed3fc600..3b9d8406840 100644 --- a/apps/desktop/src/locales/pt_BR/messages.json +++ b/apps/desktop/src/locales/pt_BR/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "settings": { "message": "Configurações" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "Verificação de E-mail Necessária" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "Você precisa verificar o seu e-mail para usar este recurso." }, diff --git a/apps/desktop/src/locales/pt_PT/messages.json b/apps/desktop/src/locales/pt_PT/messages.json index a1b9d5d6238..dda760c5ff8 100644 --- a/apps/desktop/src/locales/pt_PT/messages.json +++ b/apps/desktop/src/locales/pt_PT/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Palavra-passe mestra" + }, + "masterPassImportant": { + "message": "A sua palavra-passe mestra não pode ser recuperada se a esquecer!" + }, + "confirmMasterPassword": { + "message": "Confirmar a palavra-passe mestra" + }, + "masterPassHintLabel": { + "message": "Dica da palavra-passe mestra" + }, "settings": { "message": "Definições" }, @@ -1944,7 +1956,7 @@ "message": "Eliminação pendente" }, "webAuthnAuthenticate": { - "message": "Autenticar WebAuthn" + "message": "Autenticar o WebAuthn" }, "hideEmail": { "message": "Ocultar o meu endereço de e-mail dos destinatários." @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "Verificação de e-mail necessária" }, + "emailVerifiedV2": { + "message": "E-mail verificado" + }, "emailVerificationRequiredDesc": { "message": "É necessário verificar o seu e-mail para utilizar esta funcionalidade." }, @@ -2094,10 +2109,10 @@ } }, "leaveOrganization": { - "message": "Deixar a organização" + "message": "Sair da organização" }, "leaveOrganizationConfirmation": { - "message": "Tem a certeza de que pretende deixar esta organização?" + "message": "Tem a certeza de que pretende sair desta organização?" }, "leftOrganization": { "message": "Saiu da organização." diff --git a/apps/desktop/src/locales/ro/messages.json b/apps/desktop/src/locales/ro/messages.json index 874240889ed..2dcc23073e4 100644 --- a/apps/desktop/src/locales/ro/messages.json +++ b/apps/desktop/src/locales/ro/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "settings": { "message": "Setări" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "Verificare e-mail necesară" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "Trebuie să vă verificați e-mailul pentru a utiliza această caracteristică." }, diff --git a/apps/desktop/src/locales/ru/messages.json b/apps/desktop/src/locales/ru/messages.json index 92330eeb3aa..ecfb9437124 100644 --- a/apps/desktop/src/locales/ru/messages.json +++ b/apps/desktop/src/locales/ru/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Мастер-пароль" + }, + "masterPassImportant": { + "message": "Ваш мастер-пароль невозможно восстановить, если вы его забудете!" + }, + "confirmMasterPassword": { + "message": "Подтвердите мастер-пароль" + }, + "masterPassHintLabel": { + "message": "Подсказка к мастер-паролю" + }, "settings": { "message": "Настройки" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "Требуется подтверждение электронной почты" }, + "emailVerifiedV2": { + "message": "Email подтвержден" + }, "emailVerificationRequiredDesc": { "message": "Для использования этой функции необходимо подтвердить свою электронную почту." }, diff --git a/apps/desktop/src/locales/si/messages.json b/apps/desktop/src/locales/si/messages.json index 906181c7bdb..0ac4b332235 100644 --- a/apps/desktop/src/locales/si/messages.json +++ b/apps/desktop/src/locales/si/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "settings": { "message": "සැකසුම්" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature." }, diff --git a/apps/desktop/src/locales/sk/messages.json b/apps/desktop/src/locales/sk/messages.json index 42ebe6965b3..06ef1668103 100644 --- a/apps/desktop/src/locales/sk/messages.json +++ b/apps/desktop/src/locales/sk/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Hlavné heslo" + }, + "masterPassImportant": { + "message": "Vaše hlavné heslo sa nebude dať obnoviť, ak ho zabudnete!" + }, + "confirmMasterPassword": { + "message": "Potvrdiť hlavné heslo" + }, + "masterPassHintLabel": { + "message": "Nápoveda pre hlavné heslo" + }, "settings": { "message": "Nastavenia" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "Vyžaduje sa overenie e-mailu" }, + "emailVerifiedV2": { + "message": "Overený e-mail" + }, "emailVerificationRequiredDesc": { "message": "Na používanie tejto funkcie musíte overiť svoj e-mail." }, diff --git a/apps/desktop/src/locales/sl/messages.json b/apps/desktop/src/locales/sl/messages.json index e275b396620..9fc6f48f400 100644 --- a/apps/desktop/src/locales/sl/messages.json +++ b/apps/desktop/src/locales/sl/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "settings": { "message": "Nastavitve" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature." }, diff --git a/apps/desktop/src/locales/sr/messages.json b/apps/desktop/src/locales/sr/messages.json index e254b694c34..33b4557f27b 100644 --- a/apps/desktop/src/locales/sr/messages.json +++ b/apps/desktop/src/locales/sr/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Главна лозинка" + }, + "masterPassImportant": { + "message": "Ваша главна лозинка се не може повратити ако је заборавите!" + }, + "confirmMasterPassword": { + "message": "Потрдити главну лозинку" + }, + "masterPassHintLabel": { + "message": "Савет главне лозинке" + }, "settings": { "message": "Подешавања" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "Потребна је верификација е-поште" }, + "emailVerifiedV2": { + "message": "Имејл верификован" + }, "emailVerificationRequiredDesc": { "message": "Морате да проверите е-пошту да бисте користили ову функцију." }, diff --git a/apps/desktop/src/locales/sv/messages.json b/apps/desktop/src/locales/sv/messages.json index 3e73b2598b7..b90451b78a1 100644 --- a/apps/desktop/src/locales/sv/messages.json +++ b/apps/desktop/src/locales/sv/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Huvudlösenord" + }, + "masterPassImportant": { + "message": "Ditt huvudlösenord kan inte återställas om du glömmer det!" + }, + "confirmMasterPassword": { + "message": "Bekräfta huvudlösenord" + }, + "masterPassHintLabel": { + "message": "Huvudlösenordsledtråd" + }, "settings": { "message": "Inställningar" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "E-postverifiering krävs" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "Du måste verifiera din e-post för att använda den här funktionen." }, diff --git a/apps/desktop/src/locales/te/messages.json b/apps/desktop/src/locales/te/messages.json index 0887d769823..fa4dcc02b87 100644 --- a/apps/desktop/src/locales/te/messages.json +++ b/apps/desktop/src/locales/te/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "settings": { "message": "Settings" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature." }, diff --git a/apps/desktop/src/locales/th/messages.json b/apps/desktop/src/locales/th/messages.json index 8c0a06ec424..a0df28b7c6e 100644 --- a/apps/desktop/src/locales/th/messages.json +++ b/apps/desktop/src/locales/th/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "settings": { "message": "การตั้งค่า" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature." }, diff --git a/apps/desktop/src/locales/tr/messages.json b/apps/desktop/src/locales/tr/messages.json index b44135e8d43..c3a100394c9 100644 --- a/apps/desktop/src/locales/tr/messages.json +++ b/apps/desktop/src/locales/tr/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Ana parola" + }, + "masterPassImportant": { + "message": "Ana parolanızı unutursanız kurtaramazsınız!" + }, + "confirmMasterPassword": { + "message": "Ana parolayı onaylayın" + }, + "masterPassHintLabel": { + "message": "Ana parola ipucu" + }, "settings": { "message": "Ayarlar" }, @@ -1349,28 +1361,28 @@ "message": "This file export will be password protected and require the file password to decrypt." }, "filePassword": { - "message": "File password" + "message": "Dosya parolası" }, "exportPasswordDescription": { - "message": "This password will be used to export and import this file" + "message": "Bu parola, bu dosyayı dışa ve içe aktarmak için kullanılacaktır" }, "accountRestrictedOptionDescription": { - "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + "message": "Dışa aktarmayı şifrelemek ve içe aktarmayı yalnızca mevcut Bitwarden hesabıyla kısıtlamak için, hesabınızın kullanıcı adı ve ana parolasından türetilen hesap şifreleme anahtarınızı kullanın." }, "passwordProtected": { - "message": "Password protected" + "message": "Parola korumalı" }, "passwordProtectedOptionDescription": { - "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + "message": "Dışa aktardığınız dosyayı şifrelemek ve bir Bitwarden hesabına içe aktarmak için kullanacağınız parolayı belirleyin." }, "exportTypeHeading": { - "message": "Export type" + "message": "Dışa aktarma türü" }, "accountRestricted": { - "message": "Account restricted" + "message": "Hesap kısıtlı" }, "filePasswordAndConfirmFilePasswordDoNotMatch": { - "message": "“File password” and “Confirm file password“ do not match." + "message": "\"Dosya parolası\" ile \"Dosya parolasını onaylayın\" eşleşmiyor." }, "hCaptchaUrl": { "message": "hCaptcha adresi", @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "E-posta doğrulaması gerekiyor" }, + "emailVerifiedV2": { + "message": "E-posta doğrulandı" + }, "emailVerificationRequiredDesc": { "message": "Bu özelliği kullanmak için e-postanızı doğrulamalısınız." }, @@ -2133,7 +2148,7 @@ "message": "Hesabı değiştir" }, "alreadyHaveAccount": { - "message": "Already have an account?" + "message": "Zaten hesabınız var mı?" }, "options": { "message": "Seçenekler" @@ -2154,7 +2169,7 @@ } }, "exportingOrganizationVaultTitle": { - "message": "Exporting organization vault" + "message": "Kuruluş kasasını dışa aktarma" }, "exportingOrganizationVaultDesc": { "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", @@ -2482,25 +2497,25 @@ "message": "Giriş istendi" }, "creatingAccountOn": { - "message": "Creating account on" + "message": "Hesap oluşturuluyor:" }, "checkYourEmail": { - "message": "Check your email" + "message": "E-postanızı kontrol edin" }, "followTheLinkInTheEmailSentTo": { - "message": "Follow the link in the email sent to" + "message": "Hesabınızı oluşturmaya devam etmek için" }, "andContinueCreatingYourAccount": { - "message": "and continue creating your account." + "message": "adresine gönderdiğimiz e-postadaki bağlantıya tıklayın." }, "noEmail": { - "message": "No email?" + "message": "E-posta gelmedi mi?" }, "goBack": { - "message": "Go back" + "message": "Geri dönüp" }, "toEditYourEmailAddress": { - "message": "to edit your email address." + "message": "e-posta adresinizi düzenleyin." }, "exposedMasterPassword": { "message": "Açığa Çıkmış Ana Parola" @@ -2968,11 +2983,11 @@ } }, "back": { - "message": "Back", + "message": "Geri", "description": "Button text to navigate back" }, "removeItem": { - "message": "Remove $NAME$", + "message": "$NAME$ klasörünü kaldır", "description": "Remove a selected option, such as a folder or collection", "placeholders": { "name": { diff --git a/apps/desktop/src/locales/uk/messages.json b/apps/desktop/src/locales/uk/messages.json index 9027b8d627c..3b3d5263153 100644 --- a/apps/desktop/src/locales/uk/messages.json +++ b/apps/desktop/src/locales/uk/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Головний пароль" + }, + "masterPassImportant": { + "message": "Головний пароль неможливо відновити, якщо ви його втратите!" + }, + "confirmMasterPassword": { + "message": "Підтвердьте головний пароль" + }, + "masterPassHintLabel": { + "message": "Підказка для головного пароля" + }, "settings": { "message": "Налаштування" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "Необхідно підтвердити е-пошту" }, + "emailVerifiedV2": { + "message": "Електронну пошту підтверджено" + }, "emailVerificationRequiredDesc": { "message": "Для використання цієї функції необхідно підтвердити електронну пошту." }, diff --git a/apps/desktop/src/locales/vi/messages.json b/apps/desktop/src/locales/vi/messages.json index 6e155b82a75..26f7feec398 100644 --- a/apps/desktop/src/locales/vi/messages.json +++ b/apps/desktop/src/locales/vi/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "settings": { "message": "Cài đặt" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "Yêu cầu xác nhận danh tính qua Email" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "Bạn phải xác minh email của mình để sử dụng tính năng này." }, diff --git a/apps/desktop/src/locales/zh_CN/messages.json b/apps/desktop/src/locales/zh_CN/messages.json index 0d25428e768..a874b2ff5ef 100644 --- a/apps/desktop/src/locales/zh_CN/messages.json +++ b/apps/desktop/src/locales/zh_CN/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "主密码" + }, + "masterPassImportant": { + "message": "主密码忘记后,将无法恢复!" + }, + "confirmMasterPassword": { + "message": "确认主密码" + }, + "masterPassHintLabel": { + "message": "主密码提示" + }, "settings": { "message": "设置" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "需要验证电子邮件" }, + "emailVerifiedV2": { + "message": "电子邮箱已验证" + }, "emailVerificationRequiredDesc": { "message": "您必须验证您的电子邮件才能使用此功能。" }, diff --git a/apps/desktop/src/locales/zh_TW/messages.json b/apps/desktop/src/locales/zh_TW/messages.json index 75ac7dfb3fe..427997aa543 100644 --- a/apps/desktop/src/locales/zh_TW/messages.json +++ b/apps/desktop/src/locales/zh_TW/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "settings": { "message": "設定" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "需要驗證電子郵件" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "必須驗證您的電子郵件才能使用此功能。" }, From 7f6969c84487be1df38f900adc728a9ed9a162ae Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 12 Jul 2024 10:17:03 +0000 Subject: [PATCH 09/57] Autosync the updated translations (#10086) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/browser/src/_locales/ar/messages.json | 60 ++++++++ apps/browser/src/_locales/az/messages.json | 74 +++++++++- apps/browser/src/_locales/be/messages.json | 60 ++++++++ apps/browser/src/_locales/bg/messages.json | 68 ++++++++- apps/browser/src/_locales/bn/messages.json | 60 ++++++++ apps/browser/src/_locales/bs/messages.json | 60 ++++++++ apps/browser/src/_locales/ca/messages.json | 60 ++++++++ apps/browser/src/_locales/cs/messages.json | 60 ++++++++ apps/browser/src/_locales/cy/messages.json | 60 ++++++++ apps/browser/src/_locales/da/messages.json | 60 ++++++++ apps/browser/src/_locales/de/messages.json | 90 ++++++++++-- apps/browser/src/_locales/el/messages.json | 60 ++++++++ apps/browser/src/_locales/en_GB/messages.json | 60 ++++++++ apps/browser/src/_locales/en_IN/messages.json | 60 ++++++++ apps/browser/src/_locales/es/messages.json | 60 ++++++++ apps/browser/src/_locales/et/messages.json | 136 +++++++++++++----- apps/browser/src/_locales/eu/messages.json | 60 ++++++++ apps/browser/src/_locales/fa/messages.json | 60 ++++++++ apps/browser/src/_locales/fi/messages.json | 64 ++++++++- apps/browser/src/_locales/fil/messages.json | 60 ++++++++ apps/browser/src/_locales/fr/messages.json | 60 ++++++++ apps/browser/src/_locales/gl/messages.json | 60 ++++++++ apps/browser/src/_locales/he/messages.json | 60 ++++++++ apps/browser/src/_locales/hi/messages.json | 60 ++++++++ apps/browser/src/_locales/hr/messages.json | 60 ++++++++ apps/browser/src/_locales/hu/messages.json | 60 ++++++++ apps/browser/src/_locales/id/messages.json | 60 ++++++++ apps/browser/src/_locales/it/messages.json | 60 ++++++++ apps/browser/src/_locales/ja/messages.json | 60 ++++++++ apps/browser/src/_locales/ka/messages.json | 60 ++++++++ apps/browser/src/_locales/km/messages.json | 60 ++++++++ apps/browser/src/_locales/kn/messages.json | 60 ++++++++ apps/browser/src/_locales/ko/messages.json | 60 ++++++++ apps/browser/src/_locales/lt/messages.json | 74 +++++++++- apps/browser/src/_locales/lv/messages.json | 74 +++++++++- apps/browser/src/_locales/ml/messages.json | 60 ++++++++ apps/browser/src/_locales/mr/messages.json | 60 ++++++++ apps/browser/src/_locales/my/messages.json | 60 ++++++++ apps/browser/src/_locales/nb/messages.json | 60 ++++++++ apps/browser/src/_locales/ne/messages.json | 60 ++++++++ apps/browser/src/_locales/nl/messages.json | 60 ++++++++ apps/browser/src/_locales/nn/messages.json | 60 ++++++++ apps/browser/src/_locales/or/messages.json | 60 ++++++++ apps/browser/src/_locales/pl/messages.json | 60 ++++++++ apps/browser/src/_locales/pt_BR/messages.json | 110 ++++++++++---- apps/browser/src/_locales/pt_PT/messages.json | 66 ++++++++- apps/browser/src/_locales/ro/messages.json | 60 ++++++++ apps/browser/src/_locales/ru/messages.json | 60 ++++++++ apps/browser/src/_locales/si/messages.json | 60 ++++++++ apps/browser/src/_locales/sk/messages.json | 60 ++++++++ apps/browser/src/_locales/sl/messages.json | 60 ++++++++ apps/browser/src/_locales/sr/messages.json | 74 +++++++++- apps/browser/src/_locales/sv/messages.json | 60 ++++++++ apps/browser/src/_locales/te/messages.json | 60 ++++++++ apps/browser/src/_locales/th/messages.json | 60 ++++++++ apps/browser/src/_locales/tr/messages.json | 60 ++++++++ apps/browser/src/_locales/uk/messages.json | 60 ++++++++ apps/browser/src/_locales/vi/messages.json | 60 ++++++++ apps/browser/src/_locales/zh_CN/messages.json | 60 ++++++++ apps/browser/src/_locales/zh_TW/messages.json | 60 ++++++++ 60 files changed, 3715 insertions(+), 115 deletions(-) diff --git a/apps/browser/src/_locales/ar/messages.json b/apps/browser/src/_locales/ar/messages.json index 6aeaadd81a4..197cf3eb920 100644 --- a/apps/browser/src/_locales/ar/messages.json +++ b/apps/browser/src/_locales/ar/messages.json @@ -556,6 +556,18 @@ "security": { "message": "الأمان" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "لقد حدث خطأ ما" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "سجل كلمة المرور" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "تأكيد البريد الإلكتروني مطلوب" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "يجب عليك تأكيد بريدك الإلكتروني لاستخدام هذه الميزة. يمكنك تأكيد بريدك الإلكتروني في خزنة الويب." }, @@ -3520,6 +3544,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3559,6 +3601,24 @@ "filters": { "message": "Filters" }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Card details" }, diff --git a/apps/browser/src/_locales/az/messages.json b/apps/browser/src/_locales/az/messages.json index c9d68c88ebb..4c82d3726a3 100644 --- a/apps/browser/src/_locales/az/messages.json +++ b/apps/browser/src/_locales/az/messages.json @@ -556,6 +556,18 @@ "security": { "message": "Güvənlik" }, + "confirmMasterPassword": { + "message": "Ana parolu təsdiqlə" + }, + "masterPassword": { + "message": "Ana parol" + }, + "masterPassImportant": { + "message": "Unutsanız, ana parolunuz geri qaytarıla bilməz!" + }, + "masterPassHintLabel": { + "message": "Ana parol ipucusu" + }, "errorOccurred": { "message": "Bir xəta baş verdi" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "$TYPE$ - bax", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Parol tarixçəsi" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "E-poçt doğrulaması tələb olunur" }, + "emailVerifiedV2": { + "message": "E-poçt doğrulandı" + }, "emailVerificationRequiredDesc": { "message": "Bu özəlliyi istifadə etmək üçün e-poçtunuzu doğrulamalısınız. E-poçtunuzu veb anbarında doğrulaya bilərsiniz." }, @@ -3493,13 +3517,13 @@ "message": "Qovluğu olmayan elementlər" }, "itemDetails": { - "message": "Item details" + "message": "Element detalları" }, "itemName": { - "message": "Item name" + "message": "Element adı" }, "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "message": "\"Yalnız baxma\" icazələrinə sahib kolleksiyaları silə bilməzsiniz: $COLLECTIONS$", "placeholders": { "collections": { "content": "$1", @@ -3511,15 +3535,33 @@ "message": "Təşkilat deaktiv edildi" }, "owner": { - "message": "Owner" + "message": "Sahiblik" }, "selfOwnershipLabel": { - "message": "You", + "message": "Siz", "description": "Used as a label to indicate that the user is the owner of an item." }, "contactYourOrgAdmin": { "message": "Deaktiv edilmiş təşkilatlardakı elementlərə müraciət edilə bilməz. Kömək üçün təşkilatınızın sahibi ilə əlaqə saxlayın." }, + "additionalInformation": { + "message": "Əlavə məlumat" + }, + "itemHistory": { + "message": "Element tarixçəsi" + }, + "lastEdited": { + "message": "Son düzəliş" + }, + "ownerYou": { + "message": "Sahiblik: Siz" + }, + "linked": { + "message": "Əlaqələndirildi" + }, + "copySuccessful": { + "message": "Uğurla kopyalandı" + }, "upload": { "message": "Yüklə" }, @@ -3559,11 +3601,29 @@ "filters": { "message": "Filtrlər" }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "$ITEMNAME$ - endir", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { - "message": "Card details" + "message": "Kart detalları" }, "cardBrandDetails": { - "message": "$BRAND$ details", + "message": "$BRAND$ detalları", "placeholders": { "brand": { "content": "$1", diff --git a/apps/browser/src/_locales/be/messages.json b/apps/browser/src/_locales/be/messages.json index d855d8447f2..f772077008f 100644 --- a/apps/browser/src/_locales/be/messages.json +++ b/apps/browser/src/_locales/be/messages.json @@ -556,6 +556,18 @@ "security": { "message": "Бяспека" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "Адбылася памылка" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Гісторыя пароляў" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "Патрабуецца праверка электроннай пошты" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "Вы павінны праверыць свой адрас электроннай пошты, каб выкарыстоўваць гэту функцыю. Зрабіць гэта можна ў вэб-сховішчы." }, @@ -3520,6 +3544,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3559,6 +3601,24 @@ "filters": { "message": "Filters" }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Card details" }, diff --git a/apps/browser/src/_locales/bg/messages.json b/apps/browser/src/_locales/bg/messages.json index 360d73d41d8..74a0a5b8cad 100644 --- a/apps/browser/src/_locales/bg/messages.json +++ b/apps/browser/src/_locales/bg/messages.json @@ -556,6 +556,18 @@ "security": { "message": "Сигурност" }, + "confirmMasterPassword": { + "message": "Потвърждаване на главната парола" + }, + "masterPassword": { + "message": "Главна парола" + }, + "masterPassImportant": { + "message": "Главната парола не може да бъде възстановена, ако я забравите!" + }, + "masterPassHintLabel": { + "message": "Подсказка за главната парола" + }, "errorOccurred": { "message": "Възникна грешка" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "Преглед на $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Хронология на паролата" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "Изисква се потвърждение на е-пощата" }, + "emailVerifiedV2": { + "message": "Е-пощата е потвърдена" + }, "emailVerificationRequiredDesc": { "message": "Трябва да потвърдите е-пощата си, за да използвате тази функционалност. Можете да го направите в уеб-трезора." }, @@ -3493,13 +3517,13 @@ "message": "Елементи без папка" }, "itemDetails": { - "message": "Подробности за елемент" + "message": "Подробности за елемента" }, "itemName": { - "message": "Име на елемент" + "message": "Име на елемента" }, "cannotRemoveViewOnlyCollections": { - "message": "Не можете да премахнете колекции с права „Само за преглед“: $COLLECTIONS$", + "message": "Не можете да премахвате колекции с права „Само за преглед“: $COLLECTIONS$", "placeholders": { "collections": { "content": "$1", @@ -3520,6 +3544,24 @@ "contactYourOrgAdmin": { "message": "Записите в деактивирани организации не са достъпни. Свържете се със собственика на организацията си за помощ." }, + "additionalInformation": { + "message": "Допълнителна информация" + }, + "itemHistory": { + "message": "История на елемента" + }, + "lastEdited": { + "message": "Последна промяна" + }, + "ownerYou": { + "message": "Собственик: Вие" + }, + "linked": { + "message": "Свързано" + }, + "copySuccessful": { + "message": "Копирането е успешно" + }, "upload": { "message": "Качване" }, @@ -3559,11 +3601,29 @@ "filters": { "message": "Филтри" }, + "personalDetails": { + "message": "Лични данни" + }, + "identification": { + "message": "Идентификация" + }, + "contactInfo": { + "message": "Информация за връзка" + }, + "downloadAttachment": { + "message": "Сваляне – $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Данни за картата" }, "cardBrandDetails": { - "message": "$BRAND$ подробности", + "message": "Подробности за $BRAND$", "placeholders": { "brand": { "content": "$1", diff --git a/apps/browser/src/_locales/bn/messages.json b/apps/browser/src/_locales/bn/messages.json index cffb78f5b46..9b4d69b08dd 100644 --- a/apps/browser/src/_locales/bn/messages.json +++ b/apps/browser/src/_locales/bn/messages.json @@ -556,6 +556,18 @@ "security": { "message": "নিরাপত্তা" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "একটি ত্রুটি উৎপন্ন হয়েছে" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "পাসওয়ার্ড ইতিহাস" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "ইমেইল সত্যায়ন প্রয়োজন" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature. You can verify your email in the web vault." }, @@ -3520,6 +3544,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3559,6 +3601,24 @@ "filters": { "message": "Filters" }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Card details" }, diff --git a/apps/browser/src/_locales/bs/messages.json b/apps/browser/src/_locales/bs/messages.json index 48159dcd6d2..7d4fc507412 100644 --- a/apps/browser/src/_locales/bs/messages.json +++ b/apps/browser/src/_locales/bs/messages.json @@ -556,6 +556,18 @@ "security": { "message": "Security" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "An error has occurred" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Password history" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature. You can verify your email in the web vault." }, @@ -3520,6 +3544,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3559,6 +3601,24 @@ "filters": { "message": "Filters" }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Card details" }, diff --git a/apps/browser/src/_locales/ca/messages.json b/apps/browser/src/_locales/ca/messages.json index 3d7ae128fbc..13cd7d0b9ce 100644 --- a/apps/browser/src/_locales/ca/messages.json +++ b/apps/browser/src/_locales/ca/messages.json @@ -556,6 +556,18 @@ "security": { "message": "Seguretat" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "S'ha produït un error" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Historial de les contrasenyes" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "Es requereix verificació del correu electrònic" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "Heu de verificar el correu electrònic per utilitzar aquesta característica. Podeu verificar el vostre correu electrònic a la caixa forta web." }, @@ -3520,6 +3544,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3559,6 +3601,24 @@ "filters": { "message": "Filters" }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Card details" }, diff --git a/apps/browser/src/_locales/cs/messages.json b/apps/browser/src/_locales/cs/messages.json index 74c8ce12125..76232e6c8fa 100644 --- a/apps/browser/src/_locales/cs/messages.json +++ b/apps/browser/src/_locales/cs/messages.json @@ -556,6 +556,18 @@ "security": { "message": "Zabezpečení" }, + "confirmMasterPassword": { + "message": "Potvrzení hlavního hesla" + }, + "masterPassword": { + "message": "Hlavní heslo" + }, + "masterPassImportant": { + "message": "Pokud zapomenete Vaše hlavní heslo, nebude možné jej obnovit!" + }, + "masterPassHintLabel": { + "message": "Nápověda k hlavnímu heslu" + }, "errorOccurred": { "message": "Vyskytla se chyba" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "Zobrazit $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Historie hesel" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "Je vyžadováno ověření e-mailu" }, + "emailVerifiedV2": { + "message": "E-mail byl ověřen" + }, "emailVerificationRequiredDesc": { "message": "Abyste mohli tuto funkci používat, musíte ověřit svůj e-mail. Svůj e-mail můžete ověřit ve webovém trezoru." }, @@ -3520,6 +3544,24 @@ "contactYourOrgAdmin": { "message": "K položkám v deaktivované organizaci nemáte přístup. Požádejte o pomoc vlastníka organizace." }, + "additionalInformation": { + "message": "Další informace" + }, + "itemHistory": { + "message": "Historie položky" + }, + "lastEdited": { + "message": "Naposledy upraveno" + }, + "ownerYou": { + "message": "Vlastník: Vy" + }, + "linked": { + "message": "Propojeno" + }, + "copySuccessful": { + "message": "Kopírování bylo úspěšné" + }, "upload": { "message": "Nahrát" }, @@ -3559,6 +3601,24 @@ "filters": { "message": "Filtry" }, + "personalDetails": { + "message": "Osobní údaje" + }, + "identification": { + "message": "Identifikace" + }, + "contactInfo": { + "message": "Kontaktní informace" + }, + "downloadAttachment": { + "message": "Stahování - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Card details" }, diff --git a/apps/browser/src/_locales/cy/messages.json b/apps/browser/src/_locales/cy/messages.json index ae3bb72c0b0..1cc262b4dc5 100644 --- a/apps/browser/src/_locales/cy/messages.json +++ b/apps/browser/src/_locales/cy/messages.json @@ -556,6 +556,18 @@ "security": { "message": "Diogelwch" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "Bu gwall" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Hanes cyfrineiriau" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature. You can verify your email in the web vault." }, @@ -3520,6 +3544,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3559,6 +3601,24 @@ "filters": { "message": "Filters" }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Card details" }, diff --git a/apps/browser/src/_locales/da/messages.json b/apps/browser/src/_locales/da/messages.json index a8aeac4ef6e..6acfde47394 100644 --- a/apps/browser/src/_locales/da/messages.json +++ b/apps/browser/src/_locales/da/messages.json @@ -556,6 +556,18 @@ "security": { "message": "Sikkerhed" }, + "confirmMasterPassword": { + "message": "Bekræft hovedadgangskode" + }, + "masterPassword": { + "message": "Hovedadgangskode" + }, + "masterPassImportant": { + "message": "Hovedadgangskoden kan ikke gendannes, hvis den glemmes!" + }, + "masterPassHintLabel": { + "message": "Hovedadgangskodetip" + }, "errorOccurred": { "message": "Der er opstået en fejl" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "Vis $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Adgangskodehistorik" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "E-mailbekræftelse kræves" }, + "emailVerifiedV2": { + "message": "E-mail bekræftet" + }, "emailVerificationRequiredDesc": { "message": "Du skal bekræfte din e-mail for at bruge denne funktion. Du kan bekræfte din e-mail i web-boksen." }, @@ -3520,6 +3544,24 @@ "contactYourOrgAdmin": { "message": "Emner i deaktiverede organisationer kan ikke tilgås. Kontakt organisationsejeren for assistance." }, + "additionalInformation": { + "message": "Yderligere oplysninger" + }, + "itemHistory": { + "message": "Emnehistorik" + }, + "lastEdited": { + "message": "Senest redigeret" + }, + "ownerYou": { + "message": "Ejer: Dig" + }, + "linked": { + "message": "Linket" + }, + "copySuccessful": { + "message": "Kopieret" + }, "upload": { "message": "Upload" }, @@ -3559,6 +3601,24 @@ "filters": { "message": "Filtre" }, + "personalDetails": { + "message": "Personlige oplysninger" + }, + "identification": { + "message": "Identifikation" + }, + "contactInfo": { + "message": "Kontaktoplysninger" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Kortoplysninger" }, diff --git a/apps/browser/src/_locales/de/messages.json b/apps/browser/src/_locales/de/messages.json index 11900e883bd..446e13c1139 100644 --- a/apps/browser/src/_locales/de/messages.json +++ b/apps/browser/src/_locales/de/messages.json @@ -556,6 +556,18 @@ "security": { "message": "Sicherheit" }, + "confirmMasterPassword": { + "message": "Master-Passwort bestätigen" + }, + "masterPassword": { + "message": "Master-Passwort" + }, + "masterPassImportant": { + "message": "Dein Master-Passwort kann nicht wiederhergestellt werden, wenn du es vergisst!" + }, + "masterPassHintLabel": { + "message": "Master-Passwort-Hinweis" + }, "errorOccurred": { "message": "Ein Fehler ist aufgetaucht" }, @@ -1106,17 +1118,17 @@ "message": "Authenticator App" }, "authenticatorAppDescV2": { - "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "message": "Gib einen Code ein, der von einer Authentifizierungs-App wie dem Bitwarden Authenticator generiert wurde.", "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, "yubiKeyTitleV2": { - "message": "Yubico OTP Security Key" + "message": "Yubico OTP-Sicherheitsschlüssel" }, "yubiKeyDesc": { "message": "Verwende einen YubiKey um auf dein Konto zuzugreifen. Funtioniert mit YubiKey 4, Nano 4, 4C und NEO Geräten." }, "duoDescV2": { - "message": "Enter a code generated by Duo Security.", + "message": "Gib einen von Duo Security generierten Code ein.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -1133,7 +1145,7 @@ "message": "E-Mail" }, "emailDescV2": { - "message": "Enter a code sent to your email." + "message": "Gib einen an deine E-Mail-Adresse gesendeten Code ein." }, "selfHostedEnvironment": { "message": "Selbst gehostete Umgebung" @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "$TYPE$ ansehen", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Passwortverlauf" }, @@ -1809,7 +1830,7 @@ "message": "jederzeit." }, "byContinuingYouAgreeToThe": { - "message": "Indem Sie fortfahren, stimmen Sie unseren" + "message": "Indem du fortfährst, stimmst du den" }, "and": { "message": "und" @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "E-Mail-Verifizierung erforderlich" }, + "emailVerifiedV2": { + "message": "E-Mail-Adresse verifiziert" + }, "emailVerificationRequiredDesc": { "message": "Du musst deine E-Mail Adresse verifizieren, um diese Funktion nutzen zu können. Du kannst deine E-Mail im Web-Tresor verifizieren." }, @@ -3493,13 +3517,13 @@ "message": "Einträge ohne Ordner" }, "itemDetails": { - "message": "Item details" + "message": "Eintrag-Details" }, "itemName": { - "message": "Item name" + "message": "Eintrags-Name" }, "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "message": "Du kannst Sammlungen mit Leseberechtigung nicht entfernen: $COLLECTIONS$", "placeholders": { "collections": { "content": "$1", @@ -3511,15 +3535,33 @@ "message": "Organisation ist deaktiviert" }, "owner": { - "message": "Owner" + "message": "Besitzer" }, "selfOwnershipLabel": { - "message": "You", + "message": "Du", "description": "Used as a label to indicate that the user is the owner of an item." }, "contactYourOrgAdmin": { "message": "Auf Einträge in deaktivierten Organisationen kann nicht zugegriffen werden. Kontaktiere deinen Organisationseigentümer für Unterstützung." }, + "additionalInformation": { + "message": "Zusätzliche Informationen" + }, + "itemHistory": { + "message": "Eintrags-Verlauf" + }, + "lastEdited": { + "message": "Zuletzt bearbeitet" + }, + "ownerYou": { + "message": "Eigentümer: Du" + }, + "linked": { + "message": "Verknüpft" + }, + "copySuccessful": { + "message": "Erfolgreich kopiert" + }, "upload": { "message": "Hochladen" }, @@ -3530,7 +3572,7 @@ "message": "Die maximale Dateigröße beträgt 500 MB" }, "deleteAttachmentName": { - "message": "Datei $NAME$ löschen", + "message": "Anhang $NAME$ löschen", "placeholders": { "name": { "content": "$1", @@ -3548,22 +3590,40 @@ } }, "permanentlyDeleteAttachmentConfirmation": { - "message": "Sind Sie sich sicher, dass Sie diesen Anhang dauerhaft löschen möchten?" + "message": "Bist du sicher, dass du diesen Anhang dauerhaft löschen möchtest?" }, "premium": { "message": "Premium" }, "freeOrgsCannotUseAttachments": { - "message": "Free organizations cannot use attachments" + "message": "Kostenlose Organisationen können Anhänge nicht verwenden" }, "filters": { "message": "Filter" }, + "personalDetails": { + "message": "Persönliche Details" + }, + "identification": { + "message": "Identifikation" + }, + "contactInfo": { + "message": "Kontaktinformationen" + }, + "downloadAttachment": { + "message": "Herunterladen - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { - "message": "Card details" + "message": "Kartendetails" }, "cardBrandDetails": { - "message": "$BRAND$ details", + "message": "$BRAND$ Details", "placeholders": { "brand": { "content": "$1", diff --git a/apps/browser/src/_locales/el/messages.json b/apps/browser/src/_locales/el/messages.json index 765738bcade..9bfb8816f87 100644 --- a/apps/browser/src/_locales/el/messages.json +++ b/apps/browser/src/_locales/el/messages.json @@ -556,6 +556,18 @@ "security": { "message": "Ασφάλεια" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "Παρουσιάστηκε σφάλμα" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Ιστορικό Κωδικού" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "Απαιτείται Επαλήθευση Email" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "Πρέπει να επαληθεύσετε το email σας για να χρησιμοποιήσετε αυτή τη δυνατότητα. Μπορείτε να επαληθεύσετε το email σας στο web vault." }, @@ -3520,6 +3544,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3559,6 +3601,24 @@ "filters": { "message": "Filters" }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Card details" }, diff --git a/apps/browser/src/_locales/en_GB/messages.json b/apps/browser/src/_locales/en_GB/messages.json index 9160b95ed22..575959fee9b 100644 --- a/apps/browser/src/_locales/en_GB/messages.json +++ b/apps/browser/src/_locales/en_GB/messages.json @@ -556,6 +556,18 @@ "security": { "message": "Security" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "An error has occurred" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Password history" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature. You can verify your email in the web vault." }, @@ -3520,6 +3544,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organisations cannot be accessed. Contact your organisation owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3559,6 +3601,24 @@ "filters": { "message": "Filters" }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Card details" }, diff --git a/apps/browser/src/_locales/en_IN/messages.json b/apps/browser/src/_locales/en_IN/messages.json index d2793d4bd4e..3621dab7aa0 100644 --- a/apps/browser/src/_locales/en_IN/messages.json +++ b/apps/browser/src/_locales/en_IN/messages.json @@ -556,6 +556,18 @@ "security": { "message": "Security" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "An error has occurred" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Password history" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "Email Verification Required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature. You can verify your email in the web vault." }, @@ -3520,6 +3544,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organisations cannot be accessed. Contact your organisation owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3559,6 +3601,24 @@ "filters": { "message": "Filters" }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Card details" }, diff --git a/apps/browser/src/_locales/es/messages.json b/apps/browser/src/_locales/es/messages.json index 85cf8230698..d70e3fbe4b0 100644 --- a/apps/browser/src/_locales/es/messages.json +++ b/apps/browser/src/_locales/es/messages.json @@ -556,6 +556,18 @@ "security": { "message": "Seguridad" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "Ha ocurrido un error" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Historial de contraseñas" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "Verificación de correo electrónico requerida" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "Debes verificar tu correo electrónico para usar esta función. Puedes verificar tu correo electrónico en la caja fuerte web." }, @@ -3520,6 +3544,24 @@ "contactYourOrgAdmin": { "message": "No se puede acceder a los elementos de las organizaciones desactivadas. Ponte en contacto con el propietario de tu organización para obtener ayuda." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Subir" }, @@ -3559,6 +3601,24 @@ "filters": { "message": "Filtros" }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Card details" }, diff --git a/apps/browser/src/_locales/et/messages.json b/apps/browser/src/_locales/et/messages.json index 08f70aff2af..7dc7380dcd9 100644 --- a/apps/browser/src/_locales/et/messages.json +++ b/apps/browser/src/_locales/et/messages.json @@ -3,30 +3,30 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden Password Manager", + "message": "Bitwardeni paroolihaldur", "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": "Kodus, tööl ja teel - Bitwarden hoiustab imelihtsalt kõik su paroolid, pääsuvõtmed ja tundliku info", "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Logi oma olemasolevasse kontosse sisse või loo uus konto." }, "createAccount": { - "message": "Loo konto" + "message": "Konto loomine" }, "setAStrongPassword": { - "message": "Set a strong password" + "message": "Määra tugev parool" }, "finishCreatingYourAccountBySettingAPassword": { - "message": "Finish creating your account by setting a password" + "message": "Lõpeta konto loomine parooli luues" }, "login": { "message": "Logi sisse" }, "enterpriseSingleSignOn": { - "message": "Ettevõtte Single Sign-On" + "message": "Ettevõtte ühekordne sisselogimine" }, "cancel": { "message": "Tühista" @@ -50,7 +50,7 @@ "message": "Vihje võib abiks olla olukorras, kui oled ülemparooli unustanud." }, "masterPassHintText": { - "message": "If you forget your password, the password hint can be sent to your email. $CURRENT$/$MAXIMUM$ character maximum.", + "message": "Kui sa unustad oma parooli, saad saata parooli vihje e-mailile.\n$CURRENT$/$MAXIMUM$ tähepiirang.", "placeholders": { "current": { "content": "$1", @@ -72,13 +72,13 @@ "message": "Kaart" }, "vault": { - "message": "Hoidla" + "message": "Seif" }, "myVault": { - "message": "Minu hoidla" + "message": "Minu seif" }, "allVaults": { - "message": "Kõik hoidlad" + "message": "Kõik seifid" }, "tools": { "message": "Tööriistad" @@ -111,10 +111,10 @@ "message": "Automaatne täitmine" }, "autoFillLogin": { - "message": "Täida konto andmed" + "message": "Täida andmed automaatselt" }, "autoFillCard": { - "message": "Täida kaardi andmed" + "message": "Täida automaatselt kaardi andmed" }, "autoFillIdentity": { "message": "Täida identiteet" @@ -126,7 +126,7 @@ "message": "Kopeeri kohandatud välja nimi" }, "noMatchingLogins": { - "message": "Sobivaid kontoandmeid ei leitud." + "message": "Sobivaid kontoandmeid ei leitud" }, "noCards": { "message": "Kaardid puuduvad" @@ -144,7 +144,7 @@ "message": "Lisa identiteet" }, "unlockVaultMenu": { - "message": "Lukusta hoidla lahti" + "message": "Ava hoidla" }, "loginToVaultMenu": { "message": "Logi hoidlasse sisse" @@ -156,7 +156,7 @@ "message": "Lisa konto andmed" }, "addItem": { - "message": "Lisa kirje" + "message": "Lisa ese" }, "passwordHint": { "message": "Parooli vihje" @@ -189,25 +189,25 @@ "message": "Muuda ülemparooli" }, "continueToWebApp": { - "message": "Continue to web app?" + "message": "Jätka veebibrauseris?" }, "continueToWebAppDesc": { - "message": "Explore more features of your Bitwarden account on the web app." + "message": "Uuri teisi Bitwardeni konto funktsioone veebirakenduses." }, "continueToHelpCenter": { - "message": "Continue to Help Center?" + "message": "Kas soovid minna Abikeskusesse?" }, "continueToHelpCenterDesc": { - "message": "Learn more about how to use Bitwarden on the Help Center." + "message": "Uuri teisigi Bitwardeni kasutusvõimalusi Abikeskuses." }, "continueToBrowserExtensionStore": { - "message": "Continue to browser extension store?" + "message": "Mine edasi veebilaienduste poodi?" }, "continueToBrowserExtensionStoreDesc": { "message": "Help others find out if Bitwarden is right for them. Visit your browser's extension store and leave a rating now." }, "changeMasterPasswordOnWebConfirmation": { - "message": "You can change your master password on the Bitwarden web app." + "message": "Ülemparooli saab muuta Bitwardeni veebirakenduses." }, "fingerprintPhrase": { "message": "Sõrmejälje fraas", @@ -224,7 +224,7 @@ "message": "Logi välja" }, "aboutBitwarden": { - "message": "About Bitwarden" + "message": "Meist" }, "about": { "message": "Rakenduse info" @@ -233,10 +233,10 @@ "message": "More from Bitwarden" }, "continueToBitwardenDotCom": { - "message": "Continue to bitwarden.com?" + "message": "Mine edasi bitwarden.com-i?" }, "bitwardenForBusiness": { - "message": "Bitwarden for Business" + "message": "Bitwarden Ärikliendile" }, "bitwardenAuthenticator": { "message": "Bitwarden Authenticator" @@ -257,7 +257,7 @@ "message": "Create smooth and secure login experiences free from traditional passwords with Passwordless.dev. Learn more on the bitwarden.com website." }, "freeBitwardenFamilies": { - "message": "Free Bitwarden Families" + "message": "Tasuta Bitwarden Peredele" }, "freeBitwardenFamiliesPageDesc": { "message": "You are eligible for Free Bitwarden Families. Redeem this offer today in the web app." @@ -321,7 +321,7 @@ "message": "Loo oma kontodele tugevaid ja unikaalseid paroole." }, "bitWebVaultApp": { - "message": "Bitwarden web app" + "message": "Bitwardeni veebirakendus" }, "importItems": { "message": "Impordi andmed" @@ -409,13 +409,13 @@ "message": "Lemmik" }, "unfavorite": { - "message": "Unfavorite" + "message": "Eemalda lemmikutest" }, "itemAddedToFavorites": { - "message": "Item added to favorites" + "message": "Ese lisatud lemmikutesse" }, "itemRemovedFromFavorites": { - "message": "Item removed from favorites" + "message": "Ese eemaldatud lemmikutest" }, "notes": { "message": "Märkmed" @@ -439,7 +439,7 @@ "message": "Käivita" }, "launchWebsite": { - "message": "Launch website" + "message": "Ava Veebileht" }, "website": { "message": "Veebileht" @@ -463,10 +463,10 @@ "message": "Set up an unlock method in Settings" }, "sessionTimeoutHeader": { - "message": "Session timeout" + "message": "Sessiooni ajalõpp" }, "otherOptions": { - "message": "Other options" + "message": "Muud valikud" }, "rateExtension": { "message": "Hinda seda laiendust" @@ -509,7 +509,7 @@ "message": "Lukusta paroolihoidla" }, "lockAll": { - "message": "Lock all" + "message": "Lukusta kõik" }, "immediately": { "message": "Koheselt" @@ -556,6 +556,18 @@ "security": { "message": "Turvalisus" }, + "confirmMasterPassword": { + "message": "Kinnita ülemparool" + }, + "masterPassword": { + "message": "Ülemparool" + }, + "masterPassImportant": { + "message": "Ülemparooli ei saa taastada, kui sa selle unustama peaksid!" + }, + "masterPassHintLabel": { + "message": "Vihje ülemparoolile" + }, "errorOccurred": { "message": "Ilmnes viga" }, @@ -588,10 +600,10 @@ "message": "Konto on loodud! Võid nüüd sisse logida." }, "youSuccessfullyLoggedIn": { - "message": "You successfully logged in" + "message": "Sisselogimine õnnestus" }, "youMayCloseThisWindow": { - "message": "You may close this window" + "message": "Võid selle akna sulgeda" }, "masterPassSent": { "message": "Ülemparooli vihje saadeti sinu e-postile." @@ -616,7 +628,7 @@ "message": "Automaatne täitmine ebaõnnestus. Palun kopeeri informatsioon käsitsi." }, "totpCaptureError": { - "message": "Unable to scan QR code from the current webpage" + "message": "Ei õnnestunud skännida sellelt lehelt QR-kood" }, "totpCaptureSuccess": { "message": "Authenticator key added" @@ -631,7 +643,7 @@ "message": "Välja logitud" }, "loggedOutDesc": { - "message": "You have been logged out of your account." + "message": "Sa logisid oma kontolt välja." }, "loginExpired": { "message": "Sessioon on aegunud." @@ -779,7 +791,7 @@ "message": "Ask to update a login's password when a change is detected on a website. Applies to all logged in accounts." }, "enableUsePasskeys": { - "message": "Ask to save and use passkeys" + "message": "Küsi luba pääsuvõtmete salvestamiseks ja kasutamiseks" }, "usePasskeysDesc": { "message": "Ask to save new passkeys or log in with passkeys stored in your vault. Applies to all logged in accounts." @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Paroolide ajalugu" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "Vajalik on e-posti kinnitamine" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "Selle funktsiooni kasutamiseks pead kinnitama oma e-posti aadressi. Saad seda teha veebihoidlas." }, @@ -3520,6 +3544,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3559,6 +3601,24 @@ "filters": { "message": "Filters" }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Card details" }, diff --git a/apps/browser/src/_locales/eu/messages.json b/apps/browser/src/_locales/eu/messages.json index 1c98122849d..0b6d301d031 100644 --- a/apps/browser/src/_locales/eu/messages.json +++ b/apps/browser/src/_locales/eu/messages.json @@ -556,6 +556,18 @@ "security": { "message": "Segurtasuna" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "Akats bat gertatu da" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Pasahitz historia" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "Egiaztapen emaila beharrezkoa da" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "Emaila egiaztatu behar duzu funtzio hau erabiltzeko. Emaila web-eko kutxa gotorrean egiazta dezakezu." }, @@ -3520,6 +3544,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3559,6 +3601,24 @@ "filters": { "message": "Filters" }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Card details" }, diff --git a/apps/browser/src/_locales/fa/messages.json b/apps/browser/src/_locales/fa/messages.json index 003b8667fa5..a605b94dcbb 100644 --- a/apps/browser/src/_locales/fa/messages.json +++ b/apps/browser/src/_locales/fa/messages.json @@ -556,6 +556,18 @@ "security": { "message": "امنیت" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "خطایی رخ داده است" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "تاریخچه کلمه عبور" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "تأیید ایمیل لازم است" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "برای استفاده از این ویژگی باید ایمیل خود را تأیید کنید. می‌توانید ایمیل خود را در گاوصندوق وب تأیید کنید." }, @@ -3520,6 +3544,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3559,6 +3601,24 @@ "filters": { "message": "Filters" }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Card details" }, diff --git a/apps/browser/src/_locales/fi/messages.json b/apps/browser/src/_locales/fi/messages.json index a8e978b0beb..15d5d85c1a1 100644 --- a/apps/browser/src/_locales/fi/messages.json +++ b/apps/browser/src/_locales/fi/messages.json @@ -556,6 +556,18 @@ "security": { "message": "Suojaus" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "Tapahtui virhe" }, @@ -1463,7 +1475,16 @@ } }, "editItemHeader": { - "message": "Muokkaa $TYPE$", + "message": "Muokkaa kohdetta $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, + "viewItemHeader": { + "message": "View $TYPE$", "placeholders": { "type": { "content": "$1", @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "Sähköpostiosoite on vahvistettava" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "Sinun on vahvistettava sähköpostiosoitteesi käyttääksesi ominaisuutta. Voit vahvistaa osoitteesi verkkoholvissa." }, @@ -3520,6 +3544,24 @@ "contactYourOrgAdmin": { "message": "Käytöstä poistettujen organisaatioiden kohteet eivät ole käytettävissä. Ole yhteydessä organisaation omistajaan saadaksesi apua." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Lähetä" }, @@ -3548,7 +3590,7 @@ } }, "permanentlyDeleteAttachmentConfirmation": { - "message": "Haluatko varmasti poistaa tämän liitteen pysyvästi?" + "message": "Haluatko varmasti poistaa liitteen pysyvästi?" }, "premium": { "message": "Premium" @@ -3559,6 +3601,24 @@ "filters": { "message": "Suodattimet" }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Kortin tiedot" }, diff --git a/apps/browser/src/_locales/fil/messages.json b/apps/browser/src/_locales/fil/messages.json index 54e7ef33b93..4a86591c30d 100644 --- a/apps/browser/src/_locales/fil/messages.json +++ b/apps/browser/src/_locales/fil/messages.json @@ -556,6 +556,18 @@ "security": { "message": "Kaligtasan" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "Nagkaroon ng error" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Kasaysayan ng Password" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "Kailangan ang pag verify ng email" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "Kailangan mong i-verify ang iyong email upang gamitin ang tampok na ito. Maaari mong i-verify ang iyong email sa web vault." }, @@ -3520,6 +3544,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3559,6 +3601,24 @@ "filters": { "message": "Filters" }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Card details" }, diff --git a/apps/browser/src/_locales/fr/messages.json b/apps/browser/src/_locales/fr/messages.json index 1278470313a..52908f8a8c5 100644 --- a/apps/browser/src/_locales/fr/messages.json +++ b/apps/browser/src/_locales/fr/messages.json @@ -556,6 +556,18 @@ "security": { "message": "Sécurité" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "Une erreur est survenue" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Historique des mots de passe" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "Vérification de courriel requise" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "Vous devez vérifier votre courriel pour utiliser cette fonctionnalité. Vous pouvez vérifier votre courriel dans le coffre web." }, @@ -3520,6 +3544,24 @@ "contactYourOrgAdmin": { "message": "Les éléments des Organisations désactivées ne sont pas accessibles. Contactez le propriétaire de votre Organisation pour obtenir de l'aide." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3559,6 +3601,24 @@ "filters": { "message": "Filtres" }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Card details" }, diff --git a/apps/browser/src/_locales/gl/messages.json b/apps/browser/src/_locales/gl/messages.json index 85239caddb5..ce9949704f5 100644 --- a/apps/browser/src/_locales/gl/messages.json +++ b/apps/browser/src/_locales/gl/messages.json @@ -556,6 +556,18 @@ "security": { "message": "Seguridade" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "Produciuse un erro" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Historial de contrasinais" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature. You can verify your email in the web vault." }, @@ -3520,6 +3544,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3559,6 +3601,24 @@ "filters": { "message": "Filters" }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Card details" }, diff --git a/apps/browser/src/_locales/he/messages.json b/apps/browser/src/_locales/he/messages.json index ca67598a2e1..0de12256696 100644 --- a/apps/browser/src/_locales/he/messages.json +++ b/apps/browser/src/_locales/he/messages.json @@ -556,6 +556,18 @@ "security": { "message": "אבטחה" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "אירעה שגיאה" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "היסטוריית סיסמאות" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature. You can verify your email in the web vault." }, @@ -3520,6 +3544,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3559,6 +3601,24 @@ "filters": { "message": "Filters" }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Card details" }, diff --git a/apps/browser/src/_locales/hi/messages.json b/apps/browser/src/_locales/hi/messages.json index 5004beb376b..3fa5152ac7b 100644 --- a/apps/browser/src/_locales/hi/messages.json +++ b/apps/browser/src/_locales/hi/messages.json @@ -556,6 +556,18 @@ "security": { "message": "सुरक्षा" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "कोई ग़लती हुई।" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "पासवर्ड इतिहास" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "ईमेल सत्यापन आवश्यक है" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "इस सुविधा का उपयोग करने के लिए आपको अपने ईमेल को सत्यापित करना होगा। आप वेब वॉल्ट में अपने ईमेल को सत्यापित कर सकते हैं।" }, @@ -3520,6 +3544,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3559,6 +3601,24 @@ "filters": { "message": "फ़िल्टर" }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Card details" }, diff --git a/apps/browser/src/_locales/hr/messages.json b/apps/browser/src/_locales/hr/messages.json index 77bc1d37612..4514d1b3cd5 100644 --- a/apps/browser/src/_locales/hr/messages.json +++ b/apps/browser/src/_locales/hr/messages.json @@ -556,6 +556,18 @@ "security": { "message": "Sigurnost" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "Došlo je do pogreške" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Povijest" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "Potrebna je potvrda e-pošte" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "Moraš ovjeriti svoju e-poštu u mrežnom trezoru za koritšenje ove značajke." }, @@ -3520,6 +3544,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3559,6 +3601,24 @@ "filters": { "message": "Filters" }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Card details" }, diff --git a/apps/browser/src/_locales/hu/messages.json b/apps/browser/src/_locales/hu/messages.json index d4106a0e555..04bd7435f6b 100644 --- a/apps/browser/src/_locales/hu/messages.json +++ b/apps/browser/src/_locales/hu/messages.json @@ -556,6 +556,18 @@ "security": { "message": "Biztonság" }, + "confirmMasterPassword": { + "message": "Mesterjelszó megerősítése" + }, + "masterPassword": { + "message": "Mesterjelszó" + }, + "masterPassImportant": { + "message": "A mesterjelszó nem állítható helyre, ha elfelejtik!" + }, + "masterPassHintLabel": { + "message": "Mesterjelszó emlékeztető" + }, "errorOccurred": { "message": "Hiba történt." }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "$TYPE$ megtekintése", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Jelszó előzmények" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "Email hitelesítés szükséges" }, + "emailVerifiedV2": { + "message": "Az email cím ellenőrzésre került." + }, "emailVerificationRequiredDesc": { "message": "A funkció használatához igazolni kell email címet. Az email cím a webtárban ellenőrizhető." }, @@ -3520,6 +3544,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "További információ" + }, + "itemHistory": { + "message": "Elem előzmény" + }, + "lastEdited": { + "message": "Utoljára szerkesztve" + }, + "ownerYou": { + "message": "Tulajdonos: Én" + }, + "linked": { + "message": "Csatolva" + }, + "copySuccessful": { + "message": "A másolás sikeres volt." + }, "upload": { "message": "Feltöltés" }, @@ -3559,6 +3601,24 @@ "filters": { "message": "Szűrők" }, + "personalDetails": { + "message": "Személyes adatok" + }, + "identification": { + "message": "Azonosítás" + }, + "contactInfo": { + "message": "Kapcsolat infó" + }, + "downloadAttachment": { + "message": "Letöltés - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Kártyaadatok" }, diff --git a/apps/browser/src/_locales/id/messages.json b/apps/browser/src/_locales/id/messages.json index 4e92f007942..f07aebf9c5f 100644 --- a/apps/browser/src/_locales/id/messages.json +++ b/apps/browser/src/_locales/id/messages.json @@ -556,6 +556,18 @@ "security": { "message": "Keamanan" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "Terjadi kesalahan" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Riwayat Kata Sandi" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "Verifikasi Email Diperlukan" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "Anda harus memverifikasi email Anda untuk menggunakan fitur ini. Anda dapat memverifikasi email Anda di brankas web." }, @@ -3520,6 +3544,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3559,6 +3601,24 @@ "filters": { "message": "Filters" }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Card details" }, diff --git a/apps/browser/src/_locales/it/messages.json b/apps/browser/src/_locales/it/messages.json index 219728ced0a..5d517cffaf4 100644 --- a/apps/browser/src/_locales/it/messages.json +++ b/apps/browser/src/_locales/it/messages.json @@ -556,6 +556,18 @@ "security": { "message": "Sicurezza" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "Si è verificato un errore" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Cronologia delle password" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "Verifica email obbligatoria" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "Devi verificare la tua email per usare questa funzionalità. Puoi verificare la tua email nella cassaforte web." }, @@ -3520,6 +3544,24 @@ "contactYourOrgAdmin": { "message": "Non puoi accedere agli elementi nelle organizzazioni disattivate. Contatta il proprietario della tua organizzazione per ricevere assistenza." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3559,6 +3601,24 @@ "filters": { "message": "Filters" }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Card details" }, diff --git a/apps/browser/src/_locales/ja/messages.json b/apps/browser/src/_locales/ja/messages.json index f8ead624a11..6e001615899 100644 --- a/apps/browser/src/_locales/ja/messages.json +++ b/apps/browser/src/_locales/ja/messages.json @@ -556,6 +556,18 @@ "security": { "message": "セキュリティ" }, + "confirmMasterPassword": { + "message": "マスターパスワードの確認" + }, + "masterPassword": { + "message": "マスターパスワード" + }, + "masterPassImportant": { + "message": "マスターパスワードを忘れた場合は復元できません!" + }, + "masterPassHintLabel": { + "message": "マスターパスワードのヒント" + }, "errorOccurred": { "message": "エラーが発生しました" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "$TYPE$ を表示", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "パスワードの履歴" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "メールアドレスの確認が必要です" }, + "emailVerifiedV2": { + "message": "メールアドレスを認証しました" + }, "emailVerificationRequiredDesc": { "message": "この機能を使用するにはメールアドレスを確認する必要があります。ウェブ保管庫でメールアドレスを確認できます。" }, @@ -3520,6 +3544,24 @@ "contactYourOrgAdmin": { "message": "無効化された組織のアイテムにアクセスすることはできません。組織の所有者に連絡してください。" }, + "additionalInformation": { + "message": "その他の情報" + }, + "itemHistory": { + "message": "アイテム履歴" + }, + "lastEdited": { + "message": "最終更新日" + }, + "ownerYou": { + "message": "所有者: あなた" + }, + "linked": { + "message": "リンク済" + }, + "copySuccessful": { + "message": "コピーしました" + }, "upload": { "message": "アップロード" }, @@ -3559,6 +3601,24 @@ "filters": { "message": "フィルター" }, + "personalDetails": { + "message": "個人情報" + }, + "identification": { + "message": "ID" + }, + "contactInfo": { + "message": "連絡先情報" + }, + "downloadAttachment": { + "message": "ダウンロード - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "カード情報" }, diff --git a/apps/browser/src/_locales/ka/messages.json b/apps/browser/src/_locales/ka/messages.json index 4c2ca642151..281de94b0d4 100644 --- a/apps/browser/src/_locales/ka/messages.json +++ b/apps/browser/src/_locales/ka/messages.json @@ -556,6 +556,18 @@ "security": { "message": "უსაფრთხოება" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "დაფიქსირდა შეცდომა" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Password history" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature. You can verify your email in the web vault." }, @@ -3520,6 +3544,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3559,6 +3601,24 @@ "filters": { "message": "Filters" }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Card details" }, diff --git a/apps/browser/src/_locales/km/messages.json b/apps/browser/src/_locales/km/messages.json index d1412681ac2..f66a8d3c08a 100644 --- a/apps/browser/src/_locales/km/messages.json +++ b/apps/browser/src/_locales/km/messages.json @@ -556,6 +556,18 @@ "security": { "message": "Security" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "An error has occurred" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Password history" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature. You can verify your email in the web vault." }, @@ -3520,6 +3544,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3559,6 +3601,24 @@ "filters": { "message": "Filters" }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Card details" }, diff --git a/apps/browser/src/_locales/kn/messages.json b/apps/browser/src/_locales/kn/messages.json index ebc97ca1220..8f41e8fe20c 100644 --- a/apps/browser/src/_locales/kn/messages.json +++ b/apps/browser/src/_locales/kn/messages.json @@ -556,6 +556,18 @@ "security": { "message": "ಭದ್ರತೆ" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "ದೋಷ ಸಂಭವಿಸಿದೆ" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "ಪಾಸ್ವರ್ಡ್ ಇತಿಹಾಸ" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "ಇಮೇಲ್ ಪರಿಶೀಲನೆ ಅಗತ್ಯವಿದೆ" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "ಈ ವೈಶಿಷ್ಟ್ಯವನ್ನು ಬಳಸಲು ನಿಮ್ಮ ಇಮೇಲ್ ಅನ್ನು ನೀವು ಪರಿಶೀಲಿಸಬೇಕು. ವೆಬ್ ವಾಲ್ಟ್ನಲ್ಲಿ ನಿಮ್ಮ ಇಮೇಲ್ ಅನ್ನು ನೀವು ಪರಿಶೀಲಿಸಬಹುದು." }, @@ -3520,6 +3544,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3559,6 +3601,24 @@ "filters": { "message": "Filters" }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Card details" }, diff --git a/apps/browser/src/_locales/ko/messages.json b/apps/browser/src/_locales/ko/messages.json index 5b33655c21a..6da6f8d5e70 100644 --- a/apps/browser/src/_locales/ko/messages.json +++ b/apps/browser/src/_locales/ko/messages.json @@ -556,6 +556,18 @@ "security": { "message": "보안" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "오류가 발생했습니다" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "비밀번호 변경 기록" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "이메일 인증 필요함" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "이 기능을 사용하려면 이메일 인증이 필요합니다. 웹 보관함에서 이메일을 인증할 수 있습니다." }, @@ -3520,6 +3544,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3559,6 +3601,24 @@ "filters": { "message": "Filters" }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Card details" }, diff --git a/apps/browser/src/_locales/lt/messages.json b/apps/browser/src/_locales/lt/messages.json index e773c9a83b0..7e9cf75db60 100644 --- a/apps/browser/src/_locales/lt/messages.json +++ b/apps/browser/src/_locales/lt/messages.json @@ -556,6 +556,18 @@ "security": { "message": "Apsauga" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "Įvyko klaida" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Slaptažodžio istorija" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "Reikalingas elektroninio pašto patvirtinimas" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "Turite patvirtinti savo el. paštą, kad galėtumėte naudotis šia funkcija. Savo el. pašto adresą galite patvirtinti žiniatinklio saugykloje." }, @@ -3493,13 +3517,13 @@ "message": "Items with no folder" }, "itemDetails": { - "message": "Item details" + "message": "Elemento informacija" }, "itemName": { - "message": "Item name" + "message": "Elemento pavadinimas" }, "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "message": "Negalite pašalinti kolekcijų su Peržiūrėti tik leidimus: $COLLECTIONS$", "placeholders": { "collections": { "content": "$1", @@ -3511,15 +3535,33 @@ "message": "Organization is deactivated" }, "owner": { - "message": "Owner" + "message": "Savininkas" }, "selfOwnershipLabel": { - "message": "You", + "message": "Jūs", "description": "Used as a label to indicate that the user is the owner of an item." }, "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Įkelti" }, @@ -3559,11 +3601,29 @@ "filters": { "message": "Filtrai" }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { - "message": "Card details" + "message": "Kortelės duomenys" }, "cardBrandDetails": { - "message": "$BRAND$ details", + "message": "„$BRAND$“ duomenys", "placeholders": { "brand": { "content": "$1", diff --git a/apps/browser/src/_locales/lv/messages.json b/apps/browser/src/_locales/lv/messages.json index 633f8069826..23d33bf79d7 100644 --- a/apps/browser/src/_locales/lv/messages.json +++ b/apps/browser/src/_locales/lv/messages.json @@ -556,6 +556,18 @@ "security": { "message": "Drošība" }, + "confirmMasterPassword": { + "message": "Apstiprināt galveno paroli" + }, + "masterPassword": { + "message": "Galvenā parole" + }, + "masterPassImportant": { + "message": "Galveno paroli nevar atgūt, ja tā tiek aizmirsta." + }, + "masterPassHintLabel": { + "message": "Galvenās paroles norāde" + }, "errorOccurred": { "message": "Atgadījās kļūda" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "Apskatīt $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Paroļu vēsture" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "Nepieciešama e-pasta adreses apstiprināšana" }, + "emailVerifiedV2": { + "message": "E-pasta adrese ir apliecināta" + }, "emailVerificationRequiredDesc": { "message": "Ir nepieciešams apstiprināt e-pasta adresi, lai būtu iespējams izmantot šo iespēju. To var izdarīt tīmekļa glabātavā." }, @@ -3493,13 +3517,13 @@ "message": "Vienumi bez mapes" }, "itemDetails": { - "message": "Item details" + "message": "Vienuma dati" }, "itemName": { - "message": "Item name" + "message": "Vienuma nosaukums" }, "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "message": "Nevar noņemt krājumus ar tiesībām \"Tikai skatīt\": $COLLECTIONS$", "placeholders": { "collections": { "content": "$1", @@ -3511,15 +3535,33 @@ "message": "Apvienība ir atspējota" }, "owner": { - "message": "Owner" + "message": "Īpašnieks" }, "selfOwnershipLabel": { - "message": "You", + "message": "Tu", "description": "Used as a label to indicate that the user is the owner of an item." }, "contactYourOrgAdmin": { "message": "Atspējotu apvienību vienumiem nevar piekļūt. Jāsazinās ar apvienības īpašnieku, lai iegūtu palīdzību." }, + "additionalInformation": { + "message": "Papildu informācija" + }, + "itemHistory": { + "message": "Vienuma vēsture" + }, + "lastEdited": { + "message": "Pēdējo reizi labots" + }, + "ownerYou": { + "message": "Īpašnieks: Tu" + }, + "linked": { + "message": "Saistīts" + }, + "copySuccessful": { + "message": "Ievietošana starpliktuvē veiksmīga" + }, "upload": { "message": "Augšupielādēt" }, @@ -3559,11 +3601,29 @@ "filters": { "message": "Atlases" }, + "personalDetails": { + "message": "Personiskā informācija" + }, + "identification": { + "message": "Identifikācija" + }, + "contactInfo": { + "message": "Saziņas informācija" + }, + "downloadAttachment": { + "message": "Lejupielādēt $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { - "message": "Card details" + "message": "Kartes dati" }, "cardBrandDetails": { - "message": "$BRAND$ details", + "message": "$BRAND$ dati", "placeholders": { "brand": { "content": "$1", diff --git a/apps/browser/src/_locales/ml/messages.json b/apps/browser/src/_locales/ml/messages.json index 81c3608d10a..2f13c4a25d8 100644 --- a/apps/browser/src/_locales/ml/messages.json +++ b/apps/browser/src/_locales/ml/messages.json @@ -556,6 +556,18 @@ "security": { "message": "സുരക്ഷ" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "ഒരു പിഴവ് സംഭവിച്ചിരിക്കുന്നു" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "പാസ്സ്‌വേഡ് നാൾവഴി" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature. You can verify your email in the web vault." }, @@ -3520,6 +3544,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3559,6 +3601,24 @@ "filters": { "message": "Filters" }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Card details" }, diff --git a/apps/browser/src/_locales/mr/messages.json b/apps/browser/src/_locales/mr/messages.json index 73b928ebe0c..2b6091519cf 100644 --- a/apps/browser/src/_locales/mr/messages.json +++ b/apps/browser/src/_locales/mr/messages.json @@ -556,6 +556,18 @@ "security": { "message": "Security" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "An error has occurred" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Password history" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature. You can verify your email in the web vault." }, @@ -3520,6 +3544,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3559,6 +3601,24 @@ "filters": { "message": "Filters" }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Card details" }, diff --git a/apps/browser/src/_locales/my/messages.json b/apps/browser/src/_locales/my/messages.json index d1412681ac2..f66a8d3c08a 100644 --- a/apps/browser/src/_locales/my/messages.json +++ b/apps/browser/src/_locales/my/messages.json @@ -556,6 +556,18 @@ "security": { "message": "Security" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "An error has occurred" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Password history" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature. You can verify your email in the web vault." }, @@ -3520,6 +3544,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3559,6 +3601,24 @@ "filters": { "message": "Filters" }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Card details" }, diff --git a/apps/browser/src/_locales/nb/messages.json b/apps/browser/src/_locales/nb/messages.json index 3c07e43ff96..c6876dc2e28 100644 --- a/apps/browser/src/_locales/nb/messages.json +++ b/apps/browser/src/_locales/nb/messages.json @@ -556,6 +556,18 @@ "security": { "message": "Sikkerhet" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "En feil har oppstått" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Passordhistorikk" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "E-postbekreftelse kreves" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "Du må bekrefte e-posten din for å bruke denne funksjonen. Du kan bekrefte e-postadressen din i netthvelvet." }, @@ -3520,6 +3544,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3559,6 +3601,24 @@ "filters": { "message": "Filters" }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Card details" }, diff --git a/apps/browser/src/_locales/ne/messages.json b/apps/browser/src/_locales/ne/messages.json index d1412681ac2..f66a8d3c08a 100644 --- a/apps/browser/src/_locales/ne/messages.json +++ b/apps/browser/src/_locales/ne/messages.json @@ -556,6 +556,18 @@ "security": { "message": "Security" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "An error has occurred" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Password history" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature. You can verify your email in the web vault." }, @@ -3520,6 +3544,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3559,6 +3601,24 @@ "filters": { "message": "Filters" }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Card details" }, diff --git a/apps/browser/src/_locales/nl/messages.json b/apps/browser/src/_locales/nl/messages.json index 98cd9ca449d..3f563d75fa3 100644 --- a/apps/browser/src/_locales/nl/messages.json +++ b/apps/browser/src/_locales/nl/messages.json @@ -556,6 +556,18 @@ "security": { "message": "Beveiliging" }, + "confirmMasterPassword": { + "message": "Hoofdwachtwoord bevestigen" + }, + "masterPassword": { + "message": "Hoofdwachtwoord" + }, + "masterPassImportant": { + "message": "Je kunt je hoofdwachtwoord niet herstellen als je het vergeet!" + }, + "masterPassHintLabel": { + "message": "Hoofdwachtwoordhint" + }, "errorOccurred": { "message": "Er is een fout opgetreden" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "$TYPE$ weergeven", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Geschiedenis" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "E-mailverificatie vereist" }, + "emailVerifiedV2": { + "message": "E-mailadres geverifieerd" + }, "emailVerificationRequiredDesc": { "message": "Je moet je e-mailadres verifiëren om deze functie te gebruiken. Je kunt je e-mailadres verifiëren in de kluis." }, @@ -3520,6 +3544,24 @@ "contactYourOrgAdmin": { "message": "Items in een gedeactiveerde organisatie zijn niet toegankelijk. Neem contact op met de eigenaar van je organisatie voor hulp." }, + "additionalInformation": { + "message": "Aanvullende informatie" + }, + "itemHistory": { + "message": "Itemgeschiedenis" + }, + "lastEdited": { + "message": "Laatst gewijzigd" + }, + "ownerYou": { + "message": "Eigenaar: Jij" + }, + "linked": { + "message": "Gekoppeld" + }, + "copySuccessful": { + "message": "Kopiëren gelukt" + }, "upload": { "message": "Uploaden" }, @@ -3559,6 +3601,24 @@ "filters": { "message": "Filters" }, + "personalDetails": { + "message": "Persoonlijke gegevens" + }, + "identification": { + "message": "Identificatie" + }, + "contactInfo": { + "message": "Contactgegevens" + }, + "downloadAttachment": { + "message": "$ITEMNAME$ downloaden", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Kaartgegevens" }, diff --git a/apps/browser/src/_locales/nn/messages.json b/apps/browser/src/_locales/nn/messages.json index d1412681ac2..f66a8d3c08a 100644 --- a/apps/browser/src/_locales/nn/messages.json +++ b/apps/browser/src/_locales/nn/messages.json @@ -556,6 +556,18 @@ "security": { "message": "Security" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "An error has occurred" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Password history" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature. You can verify your email in the web vault." }, @@ -3520,6 +3544,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3559,6 +3601,24 @@ "filters": { "message": "Filters" }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Card details" }, diff --git a/apps/browser/src/_locales/or/messages.json b/apps/browser/src/_locales/or/messages.json index d1412681ac2..f66a8d3c08a 100644 --- a/apps/browser/src/_locales/or/messages.json +++ b/apps/browser/src/_locales/or/messages.json @@ -556,6 +556,18 @@ "security": { "message": "Security" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "An error has occurred" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Password history" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature. You can verify your email in the web vault." }, @@ -3520,6 +3544,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3559,6 +3601,24 @@ "filters": { "message": "Filters" }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Card details" }, diff --git a/apps/browser/src/_locales/pl/messages.json b/apps/browser/src/_locales/pl/messages.json index 40ba6659fb0..0ca85d73680 100644 --- a/apps/browser/src/_locales/pl/messages.json +++ b/apps/browser/src/_locales/pl/messages.json @@ -556,6 +556,18 @@ "security": { "message": "Zabezpieczenia" }, + "confirmMasterPassword": { + "message": "Potwierdź hasło główne" + }, + "masterPassword": { + "message": "Hasło główne" + }, + "masterPassImportant": { + "message": "Twoje hasło główne nie może zostać odzyskane, jeśli je zapomnisz!" + }, + "masterPassHintLabel": { + "message": "Podpowiedź do hasła głównego" + }, "errorOccurred": { "message": "Wystąpił błąd" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "Zobacz $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Historia hasła" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "Weryfikacja adresu e-mail jest wymagana" }, + "emailVerifiedV2": { + "message": "E-mail zweryfikowany" + }, "emailVerificationRequiredDesc": { "message": "Musisz zweryfikować adres e-mail, aby korzystać z tej funkcji. Adres możesz zweryfikować w sejfie internetowym." }, @@ -3520,6 +3544,24 @@ "contactYourOrgAdmin": { "message": "Nie można uzyskać dostępu do elementów w wyłączonych organizacjach. Skontaktuj się z właścicielem organizacji, aby uzyskać pomoc." }, + "additionalInformation": { + "message": "Dodatkowe informacje" + }, + "itemHistory": { + "message": "Historia elementu" + }, + "lastEdited": { + "message": "Ostatnio edytowany" + }, + "ownerYou": { + "message": "Właściciel: Ty" + }, + "linked": { + "message": "Powiązane" + }, + "copySuccessful": { + "message": "Kopiowanie zakończone sukcesem" + }, "upload": { "message": "Wyślij" }, @@ -3559,6 +3601,24 @@ "filters": { "message": "Filtry" }, + "personalDetails": { + "message": "Dane osobowe" + }, + "identification": { + "message": "Tożsamość" + }, + "contactInfo": { + "message": "Daje kontaktowe" + }, + "downloadAttachment": { + "message": "Pobierz - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Szczegóły karty" }, diff --git a/apps/browser/src/_locales/pt_BR/messages.json b/apps/browser/src/_locales/pt_BR/messages.json index 76dd0025851..c8b665fda5b 100644 --- a/apps/browser/src/_locales/pt_BR/messages.json +++ b/apps/browser/src/_locales/pt_BR/messages.json @@ -50,7 +50,7 @@ "message": "Uma dica de senha mestra pode ajudá-lo(a) a lembrá-lo(a) caso você esqueça." }, "masterPassHintText": { - "message": "If you forget your password, the password hint can be sent to your email. $CURRENT$/$MAXIMUM$ character maximum.", + "message": "Se você esquecer sua senha, a dica de senha pode ser enviada ao seu e-mail. $CURRENT$/$MAXIMUM$ caracteres máximos.", "placeholders": { "current": { "content": "$1", @@ -186,7 +186,7 @@ "message": "Confirme a sua identidade para continuar." }, "changeMasterPassword": { - "message": "Alterar Senha Mestra" + "message": "Alterar senha mestra" }, "continueToWebApp": { "message": "Continuar no aplicativo web?" @@ -556,6 +556,18 @@ "security": { "message": "Segurança" }, + "confirmMasterPassword": { + "message": "Confirme a senha mestra" + }, + "masterPassword": { + "message": "Senha mestra" + }, + "masterPassImportant": { + "message": "Sua senha mestra não pode ser recuperada se você a esquecer!" + }, + "masterPassHintLabel": { + "message": "Dica da senha mestra" + }, "errorOccurred": { "message": "Ocorreu um erro" }, @@ -855,7 +867,7 @@ "message": "Esta senha será usada para exportar e importar este arquivo" }, "accountRestrictedOptionDescription": { - "message": "Use sua chave criptográfica da conta, derivada do nome de usuário e Senha Mestra da sua conta, para criptografar a exportação e restringir importação para apenas a conta atual do Bitwarden." + "message": "Use sua chave criptográfica da conta, derivada do nome de usuário e senha mestra da sua conta, para criptografar a exportação e restringir importação para apenas a conta atual do Bitwarden." }, "passwordProtectedOptionDescription": { "message": "Defina uma senha de arquivo para criptografar a exportação e importá-la para qualquer conta do Bitwarden usando a senha para descriptografia." @@ -1106,17 +1118,17 @@ "message": "Aplicativo de Autenticação" }, "authenticatorAppDescV2": { - "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "message": "Insira um código gerado por um aplicativo autenticador como o Bitwarden Authenticator.", "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, "yubiKeyTitleV2": { - "message": "Yubico OTP Security Key" + "message": "Chave de Segurança Yubico OTP" }, "yubiKeyDesc": { "message": "Utilize uma YubiKey para acessar a sua conta. Funciona com YubiKey 4, 4 Nano, 4C, e dispositivos NEO." }, "duoDescV2": { - "message": "Enter a code generated by Duo Security.", + "message": "Insira um código gerado pelo Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -1133,7 +1145,7 @@ "message": "E-mail" }, "emailDescV2": { - "message": "Enter a code sent to your email." + "message": "Digite o código enviado para seu e-mail." }, "selfHostedEnvironment": { "message": "Ambiente Auto-hospedado" @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "Visualizar $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Histórico de Senha" }, @@ -1629,7 +1650,7 @@ "description": "ex. A weak password. Scale: Weak -> Good -> Strong" }, "weakMasterPassword": { - "message": "Senha Mestra Fraca" + "message": "Senha mestra fraca" }, "weakMasterPasswordDesc": { "message": "A senha mestra que você selecionou está fraca. Você deve usar uma senha mestra forte (ou uma frase-passe) para proteger a sua conta Bitwarden adequadamente. Tem certeza que deseja usar esta senha mestra?" @@ -1746,7 +1767,7 @@ } }, "setMasterPassword": { - "message": "Definir Senha Mestra" + "message": "Definir senha mestra" }, "currentMasterPass": { "message": "Senha mestra atual" @@ -2164,17 +2185,20 @@ "emailVerificationRequired": { "message": "Verificação de E-mail Necessária" }, + "emailVerifiedV2": { + "message": "E-mail verificado" + }, "emailVerificationRequiredDesc": { "message": "Você precisa verificar o seu e-mail para usar este recurso. Você pode verificar seu e-mail no cofre web." }, "updatedMasterPassword": { - "message": "Senha Mestra Atualizada" + "message": "Senha mestra atualizada" }, "updateMasterPassword": { - "message": "Atualizar Senha Mestra" + "message": "Atualizar senha mestra" }, "updateMasterPasswordWarning": { - "message": "Sua Senha Mestra foi alterada recentemente por um administrador de sua organização. Para acessar o cofre, você precisa atualizá-la agora. O processo desconectará você da sessão atual, exigindo que você inicie a sessão novamente. Sessões ativas em outros dispositivos podem continuar ativas por até uma hora." + "message": "Sua senha mestra foi alterada recentemente por um administrador de sua organização. Para acessar o cofre, você precisa atualizá-la agora. O processo desconectará você da sessão atual, exigindo que você inicie a sessão novamente. Sessões ativas em outros dispositivos podem continuar ativas por até uma hora." }, "updateWeakMasterPasswordWarning": { "message": "A sua senha mestra não atende a uma ou mais das políticas da sua organização. Para acessar o cofre, você deve atualizar a sua senha mestra agora. O processo desconectará você da sessão atual, exigindo que você inicie a sessão novamente. Sessões ativas em outros dispositivos podem continuar ativas por até uma hora." @@ -2277,7 +2301,7 @@ "message": "Sair da Organização" }, "removeMasterPassword": { - "message": "Remover Senha Mestra" + "message": "Remover senha mestra" }, "removedMasterPassword": { "message": "Senha mestra removida." @@ -2576,13 +2600,13 @@ "message": "Login iniciado" }, "exposedMasterPassword": { - "message": "Senha Mestra comprometida" + "message": "Senha mestra comprometida" }, "exposedMasterPasswordDesc": { "message": "A senha foi encontrada em um vazamento de dados. Use uma senha única para proteger sua conta. Tem certeza de que deseja usar uma senha já exposta?" }, "weakAndExposedMasterPassword": { - "message": "Senha Mestra fraca e comprometida" + "message": "Senha mestra fraca e comprometida" }, "weakAndBreachedMasterPasswordDesc": { "message": "Senha fraca identificada e encontrada em um vazamento de dados. Use uma senha forte e única para proteger a sua conta. Tem certeza de que deseja usar essa senha?" @@ -2594,7 +2618,7 @@ "message": "Importante:" }, "masterPasswordHint": { - "message": "Sua Senha Mestra não pode ser recuperada se você a esquecer!" + "message": "Sua senha mestra não pode ser recuperada se você a esquecer!" }, "characterMinimum": { "message": "$LENGTH$ caracteres mínimos", @@ -2886,11 +2910,11 @@ "description": "Toast message for informing the user that auto-fill on page load has been set to the default setting." }, "turnOffMasterPasswordPromptToEditField": { - "message": "Desative o prompt de senha mestra para editar este campo", + "message": "Desative a re-solicitação de senha mestra para editar este campo", "description": "Message appearing below the autofill on load message when master password reprompt is set for a vault item." }, "toggleSideNavigation": { - "message": "Toggle side navigation" + "message": "Ativar/desativar navegação lateral" }, "skipToContent": { "message": "Ir para o conteúdo" @@ -3108,7 +3132,7 @@ "message": "Confirmar senha do arquivo" }, "exportSuccess": { - "message": "Vault data exported" + "message": "Dados do cofre exportados" }, "typePasskey": { "message": "Chave de acesso" @@ -3493,10 +3517,10 @@ "message": "Itens sem pasta" }, "itemDetails": { - "message": "Item details" + "message": "Detalhes dos item" }, "itemName": { - "message": "Item name" + "message": "Nome do item" }, "cannotRemoveViewOnlyCollections": { "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", @@ -3514,14 +3538,32 @@ "message": "Owner" }, "selfOwnershipLabel": { - "message": "You", + "message": "Você", "description": "Used as a label to indicate that the user is the owner of an item." }, "contactYourOrgAdmin": { "message": "Itens em organizações desativadas não podem ser acessados. Entre em contato com o proprietário da sua organização para obter assistência." }, + "additionalInformation": { + "message": "Informação adicional" + }, + "itemHistory": { + "message": "Histórico do item" + }, + "lastEdited": { + "message": "Última edição" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { - "message": "Upload" + "message": "Fazer upload" }, "addAttachment": { "message": "Add attachment" @@ -3554,10 +3596,28 @@ "message": "Premium" }, "freeOrgsCannotUseAttachments": { - "message": "Free organizations cannot use attachments" + "message": "Organizações gratuitas não podem usar anexos" }, "filters": { - "message": "Filters" + "message": "Filtros" + }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } }, "cardDetails": { "message": "Card details" diff --git a/apps/browser/src/_locales/pt_PT/messages.json b/apps/browser/src/_locales/pt_PT/messages.json index 5578ac37a6e..a2fe3571eda 100644 --- a/apps/browser/src/_locales/pt_PT/messages.json +++ b/apps/browser/src/_locales/pt_PT/messages.json @@ -556,6 +556,18 @@ "security": { "message": "Segurança" }, + "confirmMasterPassword": { + "message": "Confirmar a palavra-passe mestra" + }, + "masterPassword": { + "message": "Palavra-passe mestra" + }, + "masterPassImportant": { + "message": "A sua palavra-passe mestra não pode ser recuperada se a esquecer!" + }, + "masterPassHintLabel": { + "message": "Dica da palavra-passe mestra" + }, "errorOccurred": { "message": "Ocorreu um erro" }, @@ -1082,7 +1094,7 @@ "message": "Abrir novo separador" }, "webAuthnAuthenticate": { - "message": "Autenticar WebAuthn" + "message": "Autenticar o WebAuthn" }, "loginUnavailable": { "message": "Início de sessão indisponível" @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "Ver $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Histórico de palavras-passe" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "Verificação de e-mail necessária" }, + "emailVerifiedV2": { + "message": "E-mail verificado" + }, "emailVerificationRequiredDesc": { "message": "Tem de verificar o seu e-mail para utilizar esta funcionalidade. Pode verificar o seu e-mail no cofre Web." }, @@ -2274,7 +2298,7 @@ } }, "leaveOrganization": { - "message": "Deixar a organização" + "message": "Sair da organização" }, "removeMasterPassword": { "message": "Remover palavra-passe mestra" @@ -2283,7 +2307,7 @@ "message": "Palavra-passe mestra removida" }, "leaveOrganizationConfirmation": { - "message": "Tem a certeza de que pretende deixar esta organização?" + "message": "Tem a certeza de que pretende sair desta organização?" }, "leftOrganization": { "message": "Saiu da organização." @@ -3520,6 +3544,24 @@ "contactYourOrgAdmin": { "message": "Não é possível aceder aos itens de organizações desativadas. Contacte o proprietário da organização para obter assistência." }, + "additionalInformation": { + "message": "Informações adicionais" + }, + "itemHistory": { + "message": "Histórico do item" + }, + "lastEdited": { + "message": "Última edição" + }, + "ownerYou": { + "message": "Proprietário: Eu" + }, + "linked": { + "message": "Associado" + }, + "copySuccessful": { + "message": "Cópia bem-sucedida" + }, "upload": { "message": "Carregar" }, @@ -3559,6 +3601,24 @@ "filters": { "message": "Filtros" }, + "personalDetails": { + "message": "Dados pessoais" + }, + "identification": { + "message": "Identificação" + }, + "contactInfo": { + "message": "Informações de contacto" + }, + "downloadAttachment": { + "message": "Transferir - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Detalhes do cartão" }, diff --git a/apps/browser/src/_locales/ro/messages.json b/apps/browser/src/_locales/ro/messages.json index 02e92106c44..211a587cfaa 100644 --- a/apps/browser/src/_locales/ro/messages.json +++ b/apps/browser/src/_locales/ro/messages.json @@ -556,6 +556,18 @@ "security": { "message": "Securitate" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "S-a produs o eroare" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Istoric parole" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "Verificare e-mail necesară" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "Trebuie să vă verificați e-mailul pentru a utiliza această caracteristică. Puteți verifica e-mailul în seiful web." }, @@ -3520,6 +3544,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3559,6 +3601,24 @@ "filters": { "message": "Filters" }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Card details" }, diff --git a/apps/browser/src/_locales/ru/messages.json b/apps/browser/src/_locales/ru/messages.json index bb9a030a6fa..3c4d24b3ddf 100644 --- a/apps/browser/src/_locales/ru/messages.json +++ b/apps/browser/src/_locales/ru/messages.json @@ -556,6 +556,18 @@ "security": { "message": "Безопасность" }, + "confirmMasterPassword": { + "message": "Подтвердите мастер-пароль" + }, + "masterPassword": { + "message": "Мастер-пароль" + }, + "masterPassImportant": { + "message": "Ваш мастер-пароль невозможно восстановить, если вы его забудете!" + }, + "masterPassHintLabel": { + "message": "Подсказка к мастер-паролю" + }, "errorOccurred": { "message": "Произошла ошибка" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "Просмотр $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "История паролей" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "Требуется подтверждение электронной почты" }, + "emailVerifiedV2": { + "message": "Email подтвержден" + }, "emailVerificationRequiredDesc": { "message": "Для использования этой функции необходимо подтвердить ваш email. Вы можете это сделать в веб-хранилище." }, @@ -3520,6 +3544,24 @@ "contactYourOrgAdmin": { "message": "Доступ к элементам в деактивированных организациях невозможен. Обратитесь за помощью к владельцу организации." }, + "additionalInformation": { + "message": "Дополнительная информация" + }, + "itemHistory": { + "message": "История элемента" + }, + "lastEdited": { + "message": "Последнее изменение" + }, + "ownerYou": { + "message": "Владелец: вы" + }, + "linked": { + "message": "Связано" + }, + "copySuccessful": { + "message": "Скопировано успешно" + }, "upload": { "message": "Загрузить" }, @@ -3559,6 +3601,24 @@ "filters": { "message": "Фильтры" }, + "personalDetails": { + "message": "Личные данные" + }, + "identification": { + "message": "Идентификация" + }, + "contactInfo": { + "message": "Контактная информация" + }, + "downloadAttachment": { + "message": "Скачать - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Реквизиты карты" }, diff --git a/apps/browser/src/_locales/si/messages.json b/apps/browser/src/_locales/si/messages.json index 0da68c198cd..b6f6de8107e 100644 --- a/apps/browser/src/_locales/si/messages.json +++ b/apps/browser/src/_locales/si/messages.json @@ -556,6 +556,18 @@ "security": { "message": "ආරක්ෂාව" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "දෝෂයක් සිදුවී ඇත" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "මුරපද ඉතිහාසය" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "ඊමේල් සත්යාපනය අවශ්ය වේ" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "මෙම අංගය භාවිතා කිරීම සඳහා ඔබේ විද්යුත් තැපෑල සත්යාපනය කළ යුතුය. වෙබ් සුරක්ෂිතාගාරයේ ඔබගේ විද්යුත් තැපෑල සත්යාපනය කළ හැකිය." }, @@ -3520,6 +3544,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3559,6 +3601,24 @@ "filters": { "message": "Filters" }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Card details" }, diff --git a/apps/browser/src/_locales/sk/messages.json b/apps/browser/src/_locales/sk/messages.json index d190bb1925d..d125d30b634 100644 --- a/apps/browser/src/_locales/sk/messages.json +++ b/apps/browser/src/_locales/sk/messages.json @@ -556,6 +556,18 @@ "security": { "message": "Zabezpečenie" }, + "confirmMasterPassword": { + "message": "Potvrdiť hlavné heslo" + }, + "masterPassword": { + "message": "Hlavné heslo" + }, + "masterPassImportant": { + "message": "Vaše hlavné heslo sa nebude dať obnoviť, ak ho zabudnete!" + }, + "masterPassHintLabel": { + "message": "Nápoveda pre hlavné heslo" + }, "errorOccurred": { "message": "Vyskytla sa chyba" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "Zobraziť $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "História hesla" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "Vyžaduje sa overenie e-mailu" }, + "emailVerifiedV2": { + "message": "Overený e-mail" + }, "emailVerificationRequiredDesc": { "message": "Na použitie tejto funkcie musíte overiť svoj e-mail. Svoj e-mail môžete overiť vo webovom trezore." }, @@ -3520,6 +3544,24 @@ "contactYourOrgAdmin": { "message": "K položkám vo vypnutej organizácii nie je možné pristupovať. Požiadajte o pomoc vlastníka organizácie." }, + "additionalInformation": { + "message": "Ďalšie informácie" + }, + "itemHistory": { + "message": "História položky" + }, + "lastEdited": { + "message": "Posledná úprava" + }, + "ownerYou": { + "message": "Vlastník: Vy" + }, + "linked": { + "message": "Prepojené" + }, + "copySuccessful": { + "message": "Úspešne skopírované" + }, "upload": { "message": "Nahrať" }, @@ -3559,6 +3601,24 @@ "filters": { "message": "Filtre" }, + "personalDetails": { + "message": "Osobné údaje" + }, + "identification": { + "message": "Identifikácia" + }, + "contactInfo": { + "message": "Kontaktné informácie" + }, + "downloadAttachment": { + "message": "Stiahnuť – $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Podrobnosti o karte" }, diff --git a/apps/browser/src/_locales/sl/messages.json b/apps/browser/src/_locales/sl/messages.json index 39ab221327b..a7bc089c69f 100644 --- a/apps/browser/src/_locales/sl/messages.json +++ b/apps/browser/src/_locales/sl/messages.json @@ -556,6 +556,18 @@ "security": { "message": "Varnost" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "Prišlo je do napake" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Zgodovina gesel" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "Potrebna je potrditev e-naslova" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "Za uporabo te funkcionalnosti morate potrditi svoj e-naslov. To lahko storite v spletnem trezorju." }, @@ -3520,6 +3544,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3559,6 +3601,24 @@ "filters": { "message": "Filters" }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Card details" }, diff --git a/apps/browser/src/_locales/sr/messages.json b/apps/browser/src/_locales/sr/messages.json index ac20e402f10..2fadd93d1d4 100644 --- a/apps/browser/src/_locales/sr/messages.json +++ b/apps/browser/src/_locales/sr/messages.json @@ -556,6 +556,18 @@ "security": { "message": "Сигурност" }, + "confirmMasterPassword": { + "message": "Потрдити главну лозинку" + }, + "masterPassword": { + "message": "Главна Лозинка" + }, + "masterPassImportant": { + "message": "Ваша главна лозинка се не може повратити ако је заборавите!" + }, + "masterPassHintLabel": { + "message": "Савет главне лозинке" + }, "errorOccurred": { "message": "Дошло је до грешке!" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Историја Лозинке" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "Потребна је верификација е-поште" }, + "emailVerifiedV2": { + "message": "Имејл верификован" + }, "emailVerificationRequiredDesc": { "message": "Морате да потврдите е-пошту да бисте користили ову функцију. Можете да потврдите е-пошту у веб сефу." }, @@ -3493,13 +3517,13 @@ "message": "Ставке без фасцикле" }, "itemDetails": { - "message": "Item details" + "message": "Детаљи ставке" }, "itemName": { - "message": "Item name" + "message": "Име ставке" }, "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "message": "Не можете уклонити колекције са дозволама само за приказ: $COLLECTIONS$", "placeholders": { "collections": { "content": "$1", @@ -3511,15 +3535,33 @@ "message": "Организација је деактивирана" }, "owner": { - "message": "Owner" + "message": "Власник" }, "selfOwnershipLabel": { - "message": "You", + "message": "Ти", "description": "Used as a label to indicate that the user is the owner of an item." }, "contactYourOrgAdmin": { "message": "Није могуће приступити ставкама у деактивираним организацијама. Обратите се власнику ваше организације за помоћ." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Отпреми" }, @@ -3559,11 +3601,29 @@ "filters": { "message": "Филтери" }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { - "message": "Card details" + "message": "Детаљи картице" }, "cardBrandDetails": { - "message": "$BRAND$ details", + "message": "$BRAND$ детаљи", "placeholders": { "brand": { "content": "$1", diff --git a/apps/browser/src/_locales/sv/messages.json b/apps/browser/src/_locales/sv/messages.json index dc351bc6878..439c7be920d 100644 --- a/apps/browser/src/_locales/sv/messages.json +++ b/apps/browser/src/_locales/sv/messages.json @@ -556,6 +556,18 @@ "security": { "message": "Säkerhet" }, + "confirmMasterPassword": { + "message": "Bekräfta huvudlösenord" + }, + "masterPassword": { + "message": "Huvudlösenord" + }, + "masterPassImportant": { + "message": "Ditt huvudlösenord kan inte återställas om du glömmer det!" + }, + "masterPassHintLabel": { + "message": "Huvudlösenordsledtråd" + }, "errorOccurred": { "message": "Ett fel har uppstått" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "Visa $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Lösenordshistorik" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "E-postverifiering krävs" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "Du måste verifiera din e-postadress för att använda den här funktionen. Du kan verifiera din e-postadress i webbvalvet." }, @@ -3520,6 +3544,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Ytterligare information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Ägare: Du" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Ladda upp" }, @@ -3559,6 +3601,24 @@ "filters": { "message": "Filters" }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Card details" }, diff --git a/apps/browser/src/_locales/te/messages.json b/apps/browser/src/_locales/te/messages.json index d1412681ac2..f66a8d3c08a 100644 --- a/apps/browser/src/_locales/te/messages.json +++ b/apps/browser/src/_locales/te/messages.json @@ -556,6 +556,18 @@ "security": { "message": "Security" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "An error has occurred" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Password history" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature. You can verify your email in the web vault." }, @@ -3520,6 +3544,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3559,6 +3601,24 @@ "filters": { "message": "Filters" }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Card details" }, diff --git a/apps/browser/src/_locales/th/messages.json b/apps/browser/src/_locales/th/messages.json index a1e8479da87..9b02c5b8fb9 100644 --- a/apps/browser/src/_locales/th/messages.json +++ b/apps/browser/src/_locales/th/messages.json @@ -556,6 +556,18 @@ "security": { "message": "ความปลอดภัย" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "พบข้อผิดพลาด" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "ประวัติของรหัสผ่าน" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature. You can verify your email in the web vault." }, @@ -3520,6 +3544,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3559,6 +3601,24 @@ "filters": { "message": "Filters" }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Card details" }, diff --git a/apps/browser/src/_locales/tr/messages.json b/apps/browser/src/_locales/tr/messages.json index 20114a5fd8e..40133762ab9 100644 --- a/apps/browser/src/_locales/tr/messages.json +++ b/apps/browser/src/_locales/tr/messages.json @@ -556,6 +556,18 @@ "security": { "message": "Güvenlik" }, + "confirmMasterPassword": { + "message": "Ana parolayı onaylayın" + }, + "masterPassword": { + "message": "Ana parola" + }, + "masterPassImportant": { + "message": "Ana parolanızı unutursanız kurtaramazsınız!" + }, + "masterPassHintLabel": { + "message": "Ana parola ipucu" + }, "errorOccurred": { "message": "Bir hata oluştu" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Parola geçmişi" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "E-posta doğrulaması gerekiyor" }, + "emailVerifiedV2": { + "message": "E-posta doğrulandı" + }, "emailVerificationRequiredDesc": { "message": "Bu özelliği kullanmak için e-postanızı doğrulamanız gerekir. E-postanızı web kasasında doğrulayabilirsiniz." }, @@ -3520,6 +3544,24 @@ "contactYourOrgAdmin": { "message": "Pasif kuruluşlardaki kayıtlara erişilemez. Destek almak için kuruluş sahibinizle iletişime geçin." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3559,6 +3601,24 @@ "filters": { "message": "Filtreler" }, + "personalDetails": { + "message": "Kişisel bilgiler" + }, + "identification": { + "message": "Kimlik" + }, + "contactInfo": { + "message": "İletişim bilgileri" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Kart bilgileri" }, diff --git a/apps/browser/src/_locales/uk/messages.json b/apps/browser/src/_locales/uk/messages.json index bcb552ee19b..45f8e12a291 100644 --- a/apps/browser/src/_locales/uk/messages.json +++ b/apps/browser/src/_locales/uk/messages.json @@ -556,6 +556,18 @@ "security": { "message": "Безпека" }, + "confirmMasterPassword": { + "message": "Підтвердьте головний пароль" + }, + "masterPassword": { + "message": "Головний пароль" + }, + "masterPassImportant": { + "message": "Головний пароль неможливо відновити, якщо ви його втратите!" + }, + "masterPassHintLabel": { + "message": "Підказка для головного пароля" + }, "errorOccurred": { "message": "Сталася помилка" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "Переглянути $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Історія паролів" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "Необхідно підтвердити е-пошту" }, + "emailVerifiedV2": { + "message": "Електронну пошту підтверджено" + }, "emailVerificationRequiredDesc": { "message": "Для використання цієї функції необхідно підтвердити електронну пошту. Ви можете виконати підтвердження у вебсховищі." }, @@ -3520,6 +3544,24 @@ "contactYourOrgAdmin": { "message": "Елементи в деактивованих організаціях недоступні. Зверніться до власника вашої організації для отримання допомоги." }, + "additionalInformation": { + "message": "Додаткова інформація" + }, + "itemHistory": { + "message": "Історія запису" + }, + "lastEdited": { + "message": "Востаннє редаговано" + }, + "ownerYou": { + "message": "Власник: Ви" + }, + "linked": { + "message": "Пов'язано" + }, + "copySuccessful": { + "message": "Успішно скопійовано" + }, "upload": { "message": "Вивантажити" }, @@ -3559,6 +3601,24 @@ "filters": { "message": "Фільтри" }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Завантажити – $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Подробиці картки" }, diff --git a/apps/browser/src/_locales/vi/messages.json b/apps/browser/src/_locales/vi/messages.json index c63e571076c..23dea4cdd79 100644 --- a/apps/browser/src/_locales/vi/messages.json +++ b/apps/browser/src/_locales/vi/messages.json @@ -556,6 +556,18 @@ "security": { "message": "Bảo mật" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "Đã xảy ra lỗi" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Lịch sử mật khẩu" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "Yêu cầu xác nhận danh tính qua email" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "Bạn phải xác nhận email để sử dụng tính năng này. Bạn có thể xác minh email trên web." }, @@ -3520,6 +3544,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3559,6 +3601,24 @@ "filters": { "message": "Filters" }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Card details" }, diff --git a/apps/browser/src/_locales/zh_CN/messages.json b/apps/browser/src/_locales/zh_CN/messages.json index 8bb4ad8e6f4..7eae4db0ce8 100644 --- a/apps/browser/src/_locales/zh_CN/messages.json +++ b/apps/browser/src/_locales/zh_CN/messages.json @@ -556,6 +556,18 @@ "security": { "message": "安全" }, + "confirmMasterPassword": { + "message": "确认主密码" + }, + "masterPassword": { + "message": "主密码" + }, + "masterPassImportant": { + "message": "主密码忘记后,将无法恢复!" + }, + "masterPassHintLabel": { + "message": "主密码提示" + }, "errorOccurred": { "message": "发生了一个错误" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "查看 $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "密码历史记录" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "需要验证电子邮件" }, + "emailVerifiedV2": { + "message": "电子邮箱已验证" + }, "emailVerificationRequiredDesc": { "message": "您必须验证电子邮件才能使用此功能。您可以在网页密码库中验证您的电子邮件。" }, @@ -3520,6 +3544,24 @@ "contactYourOrgAdmin": { "message": "无法访问已停用组织中的项目。请联系您的组织所有者获取协助。" }, + "additionalInformation": { + "message": "更多信息" + }, + "itemHistory": { + "message": "项目历史记录" + }, + "lastEdited": { + "message": "上次编辑" + }, + "ownerYou": { + "message": "所有者:您" + }, + "linked": { + "message": "已链接" + }, + "copySuccessful": { + "message": "复制成功" + }, "upload": { "message": "上传" }, @@ -3559,6 +3601,24 @@ "filters": { "message": "筛选" }, + "personalDetails": { + "message": "个人信息" + }, + "identification": { + "message": "身份" + }, + "contactInfo": { + "message": "联系信息" + }, + "downloadAttachment": { + "message": "下载 - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "支付卡详情" }, diff --git a/apps/browser/src/_locales/zh_TW/messages.json b/apps/browser/src/_locales/zh_TW/messages.json index 446439ba2b6..0dd36e8ac9c 100644 --- a/apps/browser/src/_locales/zh_TW/messages.json +++ b/apps/browser/src/_locales/zh_TW/messages.json @@ -556,6 +556,18 @@ "security": { "message": "安全" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "發生錯誤" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "密碼歷史記錄" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "需要驗證電子郵件" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "您必須驗證您的電子郵件才能使用此功能。您可以在網頁密碼庫裡驗證您的電子郵件。" }, @@ -3520,6 +3544,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3559,6 +3601,24 @@ "filters": { "message": "Filters" }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Card details" }, From e79e4445da977147f128df29c287549351b10564 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 12 Jul 2024 11:04:05 +0000 Subject: [PATCH 10/57] Autosync the updated translations (#10092) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/browser/store/locales/bg/copy.resx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/browser/store/locales/bg/copy.resx b/apps/browser/store/locales/bg/copy.resx index 0ec0b6e3af9..5851b526ce9 100644 --- a/apps/browser/store/locales/bg/copy.resx +++ b/apps/browser/store/locales/bg/copy.resx @@ -118,7 +118,7 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden - мениджър на пароли + Bitwarden — управител на пароли У дома, на работа или на път – Битуорден защитава всички Ваши пароли, секретни ключове и лична информация. From 8e0e6fdaff59f5a636e9cb320c5fd89291f388d1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 12 Jul 2024 11:07:34 +0000 Subject: [PATCH 11/57] Autosync the updated translations (#10088) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/web/src/locales/af/messages.json | 59 +++++- apps/web/src/locales/ar/messages.json | 59 +++++- apps/web/src/locales/az/messages.json | 69 ++++++- apps/web/src/locales/be/messages.json | 59 +++++- apps/web/src/locales/bg/messages.json | 69 ++++++- apps/web/src/locales/bn/messages.json | 59 +++++- apps/web/src/locales/bs/messages.json | 59 +++++- apps/web/src/locales/ca/messages.json | 59 +++++- apps/web/src/locales/cs/messages.json | 59 +++++- apps/web/src/locales/cy/messages.json | 59 +++++- apps/web/src/locales/da/messages.json | 59 +++++- apps/web/src/locales/de/messages.json | 103 +++++++--- apps/web/src/locales/el/messages.json | 59 +++++- apps/web/src/locales/en_GB/messages.json | 61 +++++- apps/web/src/locales/en_IN/messages.json | 61 +++++- apps/web/src/locales/eo/messages.json | 59 +++++- apps/web/src/locales/es/messages.json | 59 +++++- apps/web/src/locales/et/messages.json | 235 ++++++++++++++--------- apps/web/src/locales/eu/messages.json | 59 +++++- apps/web/src/locales/fa/messages.json | 59 +++++- apps/web/src/locales/fi/messages.json | 63 +++++- apps/web/src/locales/fil/messages.json | 59 +++++- apps/web/src/locales/fr/messages.json | 59 +++++- apps/web/src/locales/gl/messages.json | 59 +++++- apps/web/src/locales/he/messages.json | 59 +++++- apps/web/src/locales/hi/messages.json | 59 +++++- apps/web/src/locales/hr/messages.json | 59 +++++- apps/web/src/locales/hu/messages.json | 59 +++++- apps/web/src/locales/id/messages.json | 59 +++++- apps/web/src/locales/it/messages.json | 59 +++++- apps/web/src/locales/ja/messages.json | 59 +++++- apps/web/src/locales/ka/messages.json | 59 +++++- apps/web/src/locales/km/messages.json | 59 +++++- apps/web/src/locales/kn/messages.json | 59 +++++- apps/web/src/locales/ko/messages.json | 59 +++++- apps/web/src/locales/lv/messages.json | 79 ++++++-- apps/web/src/locales/ml/messages.json | 59 +++++- apps/web/src/locales/mr/messages.json | 59 +++++- apps/web/src/locales/my/messages.json | 59 +++++- apps/web/src/locales/nb/messages.json | 59 +++++- apps/web/src/locales/ne/messages.json | 59 +++++- apps/web/src/locales/nl/messages.json | 59 +++++- apps/web/src/locales/nn/messages.json | 59 +++++- apps/web/src/locales/or/messages.json | 59 +++++- apps/web/src/locales/pl/messages.json | 59 +++++- apps/web/src/locales/pt_BR/messages.json | 59 +++++- apps/web/src/locales/pt_PT/messages.json | 65 ++++++- apps/web/src/locales/ro/messages.json | 59 +++++- apps/web/src/locales/ru/messages.json | 59 +++++- apps/web/src/locales/si/messages.json | 59 +++++- apps/web/src/locales/sk/messages.json | 59 +++++- apps/web/src/locales/sl/messages.json | 59 +++++- apps/web/src/locales/sr/messages.json | 69 ++++++- apps/web/src/locales/sr_CS/messages.json | 59 +++++- apps/web/src/locales/sv/messages.json | 59 +++++- apps/web/src/locales/te/messages.json | 59 +++++- apps/web/src/locales/th/messages.json | 59 +++++- apps/web/src/locales/tr/messages.json | 179 +++++++++++------ apps/web/src/locales/uk/messages.json | 59 +++++- apps/web/src/locales/vi/messages.json | 59 +++++- apps/web/src/locales/zh_CN/messages.json | 59 +++++- apps/web/src/locales/zh_TW/messages.json | 59 +++++- 62 files changed, 3612 insertions(+), 450 deletions(-) diff --git a/apps/web/src/locales/af/messages.json b/apps/web/src/locales/af/messages.json index b0174d1be29..b17efb1b04a 100644 --- a/apps/web/src/locales/af/messages.json +++ b/apps/web/src/locales/af/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "E-posadres" }, - "yourVaultIsLocked": { - "message": "U kluis is vergrendel. Verifieer u hoofwagwoord om voort te gaan." + "yourVaultIsLockedV2": { + "message": "Your vault is locked" }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Is u seker u wil voortgaan?" }, "moveSelectedItemsDesc": { - "message": "Kies ’n vouer waarheen u die $COUNT$ gekose item(s) heen wil skuif.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Scan the QR code below with your authenticator app or enter the key." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Could not load QR code. Try again or use the key below." + }, "key": { "message": "Sleutel" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "U e-pos is bevestig." }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerifiedFailed": { "message": "Unable to verify your email. Try sending a new verification email." }, @@ -7877,7 +7883,7 @@ "message": "Assign to these collections" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Select collections to assign" @@ -8439,6 +8445,9 @@ "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." }, + "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." + }, "higherKDFIterations": { "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." }, @@ -8524,5 +8533,47 @@ "noInvoicesToList": { "message": "There are no invoices to list", "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,", + "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": { + "message": "contact Bitwarden support.", + "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'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsored" + }, + "licenseAndBillingManagementDesc": { + "message": "After making updates in the Bitwarden cloud server, upload your license file to apply the most recent changes." + }, + "addToFolder": { + "message": "Add to folder" + }, + "selectFolder": { + "message": "Select folder" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/ar/messages.json b/apps/web/src/locales/ar/messages.json index b1435e7364e..9b0bac34052 100644 --- a/apps/web/src/locales/ar/messages.json +++ b/apps/web/src/locales/ar/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "عنوان البريد الإلكتروني" }, - "yourVaultIsLocked": { - "message": "خزنتك مقفلة. تحقق من كلمة المرور الرئيسية للمتابعة." + "yourVaultIsLockedV2": { + "message": "Your vault is locked" }, "uuid": { "message": "معرف المستخدم الحالي" @@ -1060,7 +1060,7 @@ "message": "Are you sure you want to continue?" }, "moveSelectedItemsDesc": { - "message": "Choose a folder that you would like to move the $COUNT$ selected item(s) to.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Scan the QR code below with your authenticator app or enter the key." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Could not load QR code. Try again or use the key below." + }, "key": { "message": "Key" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "Account email verified" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerifiedFailed": { "message": "Unable to verify your email. Try sending a new verification email." }, @@ -7877,7 +7883,7 @@ "message": "Assign to these collections" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Select collections to assign" @@ -8439,6 +8445,9 @@ "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." }, + "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." + }, "higherKDFIterations": { "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." }, @@ -8524,5 +8533,47 @@ "noInvoicesToList": { "message": "There are no invoices to list", "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,", + "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": { + "message": "contact Bitwarden support.", + "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'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsored" + }, + "licenseAndBillingManagementDesc": { + "message": "After making updates in the Bitwarden cloud server, upload your license file to apply the most recent changes." + }, + "addToFolder": { + "message": "Add to folder" + }, + "selectFolder": { + "message": "Select folder" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/az/messages.json b/apps/web/src/locales/az/messages.json index 4343b4ba6fc..4a30eb3c63d 100644 --- a/apps/web/src/locales/az/messages.json +++ b/apps/web/src/locales/az/messages.json @@ -174,7 +174,7 @@ "description": "This is the folder for uncategorized items" }, "selfOwnershipLabel": { - "message": "You", + "message": "Siz", "description": "Used as a label to indicate that the user is the owner of an item." }, "addFolder": { @@ -406,13 +406,13 @@ "message": "Element" }, "itemDetails": { - "message": "Item details" + "message": "Element detalları" }, "itemName": { - "message": "Item name" + "message": "Element adı" }, "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "message": "\"Yalnız baxma\" icazələrinə sahib kolleksiyaları silə bilməzsiniz: $COLLECTIONS$", "placeholders": { "collections": { "content": "$1", @@ -854,8 +854,8 @@ "emailAddress": { "message": "E-poçt ünvanı" }, - "yourVaultIsLocked": { - "message": "Anbarınız kilidlənib. Davam etmək üçün ana parolunuzu doğrulayın." + "yourVaultIsLockedV2": { + "message": "Anbarınız kilidlənib." }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Davam etmək istədiyinizə əminsiniz?" }, "moveSelectedItemsDesc": { - "message": "Seçdiyiniz $COUNT$ elementi daşımaq istədiyiniz qovluğu seçin.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Kimlik doğrulayıcı tətbiqinizlə aşağıdakı QR kodu skan edin və ya açarı daxil edin." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "QR kod yüklənə bilmədi. Yenidən sınayın və ya aşağıdakı açarı istifadə edin." + }, "key": { "message": "Açar" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "Hesab e-poçtu doğrulandı" }, + "emailVerifiedV2": { + "message": "E-poçt doğrulandı" + }, "emailVerifiedFailed": { "message": "E-poçtunuz doğrulana bilmir. Yeni bir doğrulama e-poçtu göndərməyə çalışın." }, @@ -7877,7 +7883,7 @@ "message": "Bu kolleksiyalara təyin et" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Elementlərin paylaşılacağı kolleksiyaları seçin. Bir kolleksiyada bir element güncəlləndikdə, bütün kolleksiyalarda əks olunacaq. Elementləri, yalnız bu kolleksiyalara müraciət edə bilən təşkilat üzvləri görə bilər." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Təyin ediləcək kolleksiyaları seçin" @@ -8439,6 +8445,9 @@ "memberAccessReportDesc": { "message": "Üzvlərin düzgün kimlik məlumatlarına müraciət etdiklərinə və onların hesabınlarının güvəndə olduğuna əmin olun. Üzv müraciəti və hesab konfiqurasiyalarının CSV-sini əldə etmək üçün bu hesabatı istifadə edin." }, + "memberAccessReportPageDesc": { + "message": "Qruplar, kolleksiyalar və kolleksiya elementləri arasında təşkilat üzvlərinin müraciətini yoxlanışdan keçirin. CSV xaricə köçürməsi, kolleksiya icazələri və hesab konfiqurasiyaları haqqında məlumatlar da daxil olmaqla hər bir üzv üçün detallı məlumat təqdim edir." + }, "higherKDFIterations": { "message": "Daha yüksək KDF iterasiyaları, ana parolunuzu təcavüzkar tərəfindən \"brute force\" hücumuna qarşı qorumağa kömək edir." }, @@ -8522,7 +8531,49 @@ "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": { - "message": "There are no invoices to list", + "message": "Sadalanacaq heç bir faktura yoxdur", "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": "Bildiriş: Bu oyun sonu, client anbar gizliliyi yaxşılaşdırılacaq və provayder üzvləri artıq client anbar elementlərinə birbaşa müraciət edə bilməyəcək. Suallar üçün", + "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": { + "message": "Bitwarden dəstəyi ilə əlaqə saxlayın.", + "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'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsorlu" + }, + "licenseAndBillingManagementDesc": { + "message": "After making updates in the Bitwarden cloud server, upload your license file to apply the most recent changes." + }, + "addToFolder": { + "message": "Add to folder" + }, + "selectFolder": { + "message": "Select folder" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/be/messages.json b/apps/web/src/locales/be/messages.json index a2f99bfe6a3..d1334b431c3 100644 --- a/apps/web/src/locales/be/messages.json +++ b/apps/web/src/locales/be/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "Адрас электроннай пошты" }, - "yourVaultIsLocked": { - "message": "Ваша сховішча заблакіравана. Увядзіце асноўны пароль для працягу." + "yourVaultIsLockedV2": { + "message": "Your vault is locked" }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Вы сапраўды хочаце працягнуць?" }, "moveSelectedItemsDesc": { - "message": "Выберыце папку ў якую вы хочаце перамясціць выбраныя элементы (колькасць: $COUNT$ шт.).", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Scan the QR code below with your authenticator app or enter the key." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Could not load QR code. Try again or use the key below." + }, "key": { "message": "Ключ" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "Ваша пошта была праверана." }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerifiedFailed": { "message": "Немагчыма праверыць вашу пошту. Паспрабуйце адправіць новы праверачны ліст." }, @@ -7877,7 +7883,7 @@ "message": "Assign to these collections" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Select collections to assign" @@ -8439,6 +8445,9 @@ "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." }, + "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." + }, "higherKDFIterations": { "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." }, @@ -8524,5 +8533,47 @@ "noInvoicesToList": { "message": "There are no invoices to list", "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,", + "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": { + "message": "contact Bitwarden support.", + "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'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsored" + }, + "licenseAndBillingManagementDesc": { + "message": "After making updates in the Bitwarden cloud server, upload your license file to apply the most recent changes." + }, + "addToFolder": { + "message": "Add to folder" + }, + "selectFolder": { + "message": "Select folder" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/bg/messages.json b/apps/web/src/locales/bg/messages.json index 963ab8a5588..e7719e3d57c 100644 --- a/apps/web/src/locales/bg/messages.json +++ b/apps/web/src/locales/bg/messages.json @@ -174,7 +174,7 @@ "description": "This is the folder for uncategorized items" }, "selfOwnershipLabel": { - "message": "You", + "message": "Вие", "description": "Used as a label to indicate that the user is the owner of an item." }, "addFolder": { @@ -406,13 +406,13 @@ "message": "Елемент" }, "itemDetails": { - "message": "Item details" + "message": "Подробности за елемента" }, "itemName": { - "message": "Item name" + "message": "Име на елемента" }, "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "message": "Не можете да премахвате колекции с права „Само за преглед“: $COLLECTIONS$", "placeholders": { "collections": { "content": "$1", @@ -854,8 +854,8 @@ "emailAddress": { "message": "Адрес на електронната поща" }, - "yourVaultIsLocked": { - "message": "Трезорът е заключен — въведете главната си парола, за да продължите." + "yourVaultIsLockedV2": { + "message": "Трезорът Ви е заключен." }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Наистина ли искате да продължите?" }, "moveSelectedItemsDesc": { - "message": "Избор на папка, в която да се преместят $COUNT$ избрани записи.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Сканирайте QR-кода по-долу с приложението за удостоверяване, или въведете ключа." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "QR-кодът не може да бъде зареден. Опитайте отново или използвайте ключа по-долу." + }, "key": { "message": "Ключ" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "Адресът на електронната ви поща е потвърден." }, + "emailVerifiedV2": { + "message": "Е-пощата е потвърдена" + }, "emailVerifiedFailed": { "message": "Адресът на електронната ви поща не е потвърден. Пробвайте да изпратите ново писмо за потвърждение." }, @@ -7877,7 +7883,7 @@ "message": "Свързване с тези колекции" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Изберете колекциите, с които да бъдат споделени тези елементи. Когато даден елемент бъде променен в една колекция, промяната ще бъде отразена във всички колекции. Само членовете на организацията с достъп до тези колекции ще могат да виждат елементите." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Изберете колекции за свързване" @@ -8439,6 +8445,9 @@ "memberAccessReportDesc": { "message": "Уверете се, че членовете имат достъп до правилните идентификационни данни, както и че регистрациите им са защитени. Използвайте този доклад, за да получите файл CSV с достъпа на членовете и настройките на регистрациите им." }, + "memberAccessReportPageDesc": { + "message": "Направете проверка на достъпа на членовете до групи, колекции и елементи в колекциите. Изнасянето на на данните като файл CSV предоставя подробна разбивка по членове, включително информация относно правата за колекции и настройките на регистрациите." + }, "higherKDFIterations": { "message": "По-високите стойности за броя на повторения на KDF може да защитят главната Ви парола от атаки тип „груба сила“." }, @@ -8522,7 +8531,49 @@ "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": { - "message": "There are no invoices to list", + "message": "Няма фактури за показване", "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": "Обявление: По-късно този месец поверителността на клиентския трезор ще бъде подобрена и членовете на доставчик вече няма да имат директен достъп до елементите в клиентския трезор. Ако имате въпроси,", + "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": { + "message": "свържете се с поддръжката на Битуорден.", + "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'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Спонсорирано" + }, + "licenseAndBillingManagementDesc": { + "message": "След като направите промени в облачния сървър на Битуорден, качете файла с лиценза си, за да приложите последните промени." + }, + "addToFolder": { + "message": "Добавяне в папка" + }, + "selectFolder": { + "message": "Изберете папка" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ ще бъдат преместени завинаги в избраната организация. Вече няма да притежавате тези елементи.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ ще бъдат преместени завинаги в $ORG$. Вече няма да притежавате тези елементи.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/bn/messages.json b/apps/web/src/locales/bn/messages.json index bfa7703dcdf..c07835d7dc4 100644 --- a/apps/web/src/locales/bn/messages.json +++ b/apps/web/src/locales/bn/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "ইমেইল ঠিকানা" }, - "yourVaultIsLocked": { - "message": "আপনার ভল্ট লক করা আছে। চালিয়ে যেতে আপনার মূল পাসওয়ার্ডটি যাচাই করান।" + "yourVaultIsLockedV2": { + "message": "Your vault is locked" }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Are you sure you want to continue?" }, "moveSelectedItemsDesc": { - "message": "Choose a folder that you would like to move the $COUNT$ selected item(s) to.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Scan the QR code below with your authenticator app or enter the key." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Could not load QR code. Try again or use the key below." + }, "key": { "message": "Key" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "Account email verified" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerifiedFailed": { "message": "Unable to verify your email. Try sending a new verification email." }, @@ -7877,7 +7883,7 @@ "message": "Assign to these collections" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Select collections to assign" @@ -8439,6 +8445,9 @@ "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." }, + "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." + }, "higherKDFIterations": { "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." }, @@ -8524,5 +8533,47 @@ "noInvoicesToList": { "message": "There are no invoices to list", "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,", + "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": { + "message": "contact Bitwarden support.", + "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'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsored" + }, + "licenseAndBillingManagementDesc": { + "message": "After making updates in the Bitwarden cloud server, upload your license file to apply the most recent changes." + }, + "addToFolder": { + "message": "Add to folder" + }, + "selectFolder": { + "message": "Select folder" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/bs/messages.json b/apps/web/src/locales/bs/messages.json index 5789ea5cf74..5485a872aeb 100644 --- a/apps/web/src/locales/bs/messages.json +++ b/apps/web/src/locales/bs/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "Email adresa" }, - "yourVaultIsLocked": { - "message": "Your vault is locked. Verify your master password to continue." + "yourVaultIsLockedV2": { + "message": "Your vault is locked" }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Are you sure you want to continue?" }, "moveSelectedItemsDesc": { - "message": "Choose a folder that you would like to move the $COUNT$ selected item(s) to.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Scan the QR code below with your authenticator app or enter the key." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Could not load QR code. Try again or use the key below." + }, "key": { "message": "Key" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "Account email verified" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerifiedFailed": { "message": "Unable to verify your email. Try sending a new verification email." }, @@ -7877,7 +7883,7 @@ "message": "Assign to these collections" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Select collections to assign" @@ -8439,6 +8445,9 @@ "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." }, + "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." + }, "higherKDFIterations": { "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." }, @@ -8524,5 +8533,47 @@ "noInvoicesToList": { "message": "There are no invoices to list", "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,", + "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": { + "message": "contact Bitwarden support.", + "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'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsored" + }, + "licenseAndBillingManagementDesc": { + "message": "After making updates in the Bitwarden cloud server, upload your license file to apply the most recent changes." + }, + "addToFolder": { + "message": "Add to folder" + }, + "selectFolder": { + "message": "Select folder" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/ca/messages.json b/apps/web/src/locales/ca/messages.json index d768fe09632..51eeff484dd 100644 --- a/apps/web/src/locales/ca/messages.json +++ b/apps/web/src/locales/ca/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "Adreça electrònica" }, - "yourVaultIsLocked": { - "message": "La caixa forta està bloquejada. Verifiqueu la contrasenya mestra per continuar." + "yourVaultIsLockedV2": { + "message": "Your vault is locked" }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Esteu segur que voleu continuar?" }, "moveSelectedItemsDesc": { - "message": "Trieu una carpeta a la que vulgueu desplaçar els $COUNT$ elements seleccionats.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Scan the QR code below with your authenticator app or enter the key." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Could not load QR code. Try again or use the key below." + }, "key": { "message": "Clau" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "S'ha verificat el vostre correu electrònic." }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerifiedFailed": { "message": "No es pot verificar el vostre correu electrònic. Proveu d'enviar un nou correu electrònic de verificació." }, @@ -7877,7 +7883,7 @@ "message": "Assigna a aquestes col·leccions" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Seleccioneu les col·leccions amb les quals es compartiran els elements. Una vegada que un element s'actualitza en una col·lecció, es reflectirà a totes les col·leccions. Només els membres de l'organització amb accés a aquestes col·leccions podran veure els elements." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Seleccioneu les col·leccions per assignar" @@ -8439,6 +8445,9 @@ "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." }, + "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." + }, "higherKDFIterations": { "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." }, @@ -8524,5 +8533,47 @@ "noInvoicesToList": { "message": "There are no invoices to list", "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,", + "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": { + "message": "contact Bitwarden support.", + "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'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsored" + }, + "licenseAndBillingManagementDesc": { + "message": "After making updates in the Bitwarden cloud server, upload your license file to apply the most recent changes." + }, + "addToFolder": { + "message": "Add to folder" + }, + "selectFolder": { + "message": "Select folder" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/cs/messages.json b/apps/web/src/locales/cs/messages.json index 9a94d9b0b25..581345d155c 100644 --- a/apps/web/src/locales/cs/messages.json +++ b/apps/web/src/locales/cs/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "E-mailová adresa" }, - "yourVaultIsLocked": { - "message": "Váš trezor je uzamčen. Pro pokračování musíte zadat hlavní heslo." + "yourVaultIsLockedV2": { + "message": "Your vault is locked" }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Opravdu chcete pokračovat?" }, "moveSelectedItemsDesc": { - "message": "Vyberte složku, do které chcete přesunout $COUNT$ vybraných položek.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Naskenujte QR kód pomocí Vaší ověřovací aplikace nebo zadejte klíč." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "QR kód nelze načíst. Zkuste to znovu nebo použijte klíč níže." + }, "key": { "message": "Klíč" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "Vaše e-mailová adresa byla ověřena" }, + "emailVerifiedV2": { + "message": "E-mail byl ověřen" + }, "emailVerifiedFailed": { "message": "Nelze ověřit Váš e-mail. Zkuste odeslat nový ověřovací e-mail." }, @@ -7877,7 +7883,7 @@ "message": "Přiřadit k těmto kolekcím" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Vyberte kolekce, se kterými budou položky sdíleny. Jakmile bude položka aktualizována v jedné kolekci, bude zobrazena ve všech kolekcích. Jen členové organizace s přístupem k těmto kolekcím budou moci vidět položky." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Vyberte kolekce pro přiřazení" @@ -8439,6 +8445,9 @@ "memberAccessReportDesc": { "message": "Ujistěte se, že členové mají přístup k správným údajům a jejich účty jsou bezpečné. Použijte tuto zprávu k získání CSV přístupu členů a konfigurací účtu." }, + "memberAccessReportPageDesc": { + "message": "Audit přístupu členů organizace ke skupinám, kolekcím a položkám kolekcí. Export CSV poskytuje podrobný rozpis pro jednotlivé členy, včetně informací o oprávněních ke kolekcím a konfiguracích účtů." + }, "higherKDFIterations": { "message": "Více iterací KDF Vám může pomoci ochránit hlavní heslo před útočníkem, který by ho vylákal hrubou silou." }, @@ -8524,5 +8533,47 @@ "noInvoicesToList": { "message": "Žádné faktury k zobrazení", "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": "Upozornění: Koncem tohoto měsíce bude vylepšena ochrana osobních údajů v klientském trezoru a členové poskytovatele již nebudou mít přímý přístup k položkám klientského trezoru. V případě dotazů se", + "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": { + "message": "obraťte na podporu společnosti Bitwarden.", + "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'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponzorováno" + }, + "licenseAndBillingManagementDesc": { + "message": "Po provedení aktualizací na cloudovém serveru Bitwardenu nahrajte váš licenční soubor pro použití nejnovějších změn." + }, + "addToFolder": { + "message": "Přidat do složky" + }, + "selectFolder": { + "message": "Vybrat složku" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ bude trvale převedeno do vybrané organizace. Tyto položky již nebudete vlastnit.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ bude trvale převedeno do $ORG$. Tyto položky již nebudete vlastnit.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/cy/messages.json b/apps/web/src/locales/cy/messages.json index fe9a3e8d521..4244b3f41c0 100644 --- a/apps/web/src/locales/cy/messages.json +++ b/apps/web/src/locales/cy/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "Email address" }, - "yourVaultIsLocked": { - "message": "Your vault is locked. Verify your master password to continue." + "yourVaultIsLockedV2": { + "message": "Your vault is locked" }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Are you sure you want to continue?" }, "moveSelectedItemsDesc": { - "message": "Choose a folder that you would like to move the $COUNT$ selected item(s) to.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Scan the QR code below with your authenticator app or enter the key." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Could not load QR code. Try again or use the key below." + }, "key": { "message": "Key" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "Account email verified" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerifiedFailed": { "message": "Unable to verify your email. Try sending a new verification email." }, @@ -7877,7 +7883,7 @@ "message": "Assign to these collections" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Select collections to assign" @@ -8439,6 +8445,9 @@ "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." }, + "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." + }, "higherKDFIterations": { "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." }, @@ -8524,5 +8533,47 @@ "noInvoicesToList": { "message": "There are no invoices to list", "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,", + "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": { + "message": "contact Bitwarden support.", + "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'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsored" + }, + "licenseAndBillingManagementDesc": { + "message": "After making updates in the Bitwarden cloud server, upload your license file to apply the most recent changes." + }, + "addToFolder": { + "message": "Add to folder" + }, + "selectFolder": { + "message": "Select folder" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/da/messages.json b/apps/web/src/locales/da/messages.json index 2b4f4f5abfe..44b4e775af5 100644 --- a/apps/web/src/locales/da/messages.json +++ b/apps/web/src/locales/da/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "E-mailadresse" }, - "yourVaultIsLocked": { - "message": "Din boks er låst. Bekræft din hovedadgangskode for at fortsætte." + "yourVaultIsLockedV2": { + "message": "Boksen er låst." }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Sikker på, at du vil fortsætte?" }, "moveSelectedItemsDesc": { - "message": "Vælg en mappe, hvortil de(t) $COUNT$ valgte emne(r) ønskes flyttet.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Skan QR-koden nedenfor med godkendelses-appen eller angiv nøglen." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Kunne ikke indlæse QR-kode. Forsøg igen eller brug nøglen nedenfor." + }, "key": { "message": "Nøgle" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "Kontoe-mail bekræftet" }, + "emailVerifiedV2": { + "message": "E-mail bekræftet" + }, "emailVerifiedFailed": { "message": "Kan ikke bekræfte din e-mail. Prøv at sende en ny verifikations-email." }, @@ -7877,7 +7883,7 @@ "message": "Tildel til samlinger" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Vælg de samlinger som emnerne vil blive delt med. Når et emne er opdateret i en samling, vil det blive afspejlet i alle samlinger. Kun organisationsmedlemmer med adgang til disse samlinger vil kunne se emnerne." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Vælg samlinger at tildele" @@ -8439,6 +8445,9 @@ "memberAccessReportDesc": { "message": "Sikr, at medlemmerne har adgang til de rigtige legitimationsoplysninger, og at deres konti er sikre. Brug denne rapport til at få en CSV over medlemsadgang og kontoopsætninger." }, + "memberAccessReportPageDesc": { + "message": "Inspicér organisationsmedlemsadgang på tværs af grupper, samlinger og samlingsemner. CSV-eksporten giver en detaljeret opdeling pr. medlem, herunder oplysninger om samlingstilladelser og kontoopsætninger." + }, "higherKDFIterations": { "message": "Højere KDF-iterationer kan hjælpe med at beskytte hovedadgangskoden mod brute force-angreb." }, @@ -8524,5 +8533,47 @@ "noInvoicesToList": { "message": "Ingen fakturaer at vise", "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": "Bemærk: Senere på måneden forbedres klientboksfortroligheden, og udbydermedlemmer vil ikke længere kunne tilgå klientboksemner direkte. For evt. spørgsmål,", + "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": { + "message": "contact Bitwarden support.", + "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'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponseret" + }, + "licenseAndBillingManagementDesc": { + "message": "Efter at have foretaget opdateringer på Bitwarden cloud-serveren, uploade licensfilen for at anvende de seneste ændringer." + }, + "addToFolder": { + "message": "Føj til mappe" + }, + "selectFolder": { + "message": "Vælg mappe" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ overføres permanent til den valgte organisation. Man vil ikke længere eje disse emner.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ overføres permanent til $ORG$. Man vil ikke længere eje disse emner.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/de/messages.json b/apps/web/src/locales/de/messages.json index 37c5d3f6260..ab55254dfbd 100644 --- a/apps/web/src/locales/de/messages.json +++ b/apps/web/src/locales/de/messages.json @@ -174,7 +174,7 @@ "description": "This is the folder for uncategorized items" }, "selfOwnershipLabel": { - "message": "You", + "message": "Du", "description": "Used as a label to indicate that the user is the owner of an item." }, "addFolder": { @@ -406,13 +406,13 @@ "message": "Eintrag" }, "itemDetails": { - "message": "Item details" + "message": "Eintrag-Details" }, "itemName": { - "message": "Item name" + "message": "Eintrags-Name" }, "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "message": "Du kannst Sammlungen mit Leseberechtigung nicht entfernen: $COLLECTIONS$", "placeholders": { "collections": { "content": "$1", @@ -854,8 +854,8 @@ "emailAddress": { "message": "E-Mail-Adresse" }, - "yourVaultIsLocked": { - "message": "Dein Tresor ist gesperrt. Überprüfe dein Master-Passwort, um fortzufahren." + "yourVaultIsLockedV2": { + "message": "Dein Tresor ist gesperrt." }, "uuid": { "message": "UUID" @@ -982,17 +982,17 @@ "message": "Authenticator App" }, "authenticatorAppDescV2": { - "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "message": "Gib einen Code ein, der von einer Authentifizierungs-App wie dem Bitwarden Authenticator generiert wurde.", "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, "yubiKeyTitleV2": { - "message": "Yubico OTP security key" + "message": "Yubico OTP-Sicherheitsschlüssel" }, "yubiKeyDesc": { - "message": "Verwenden Sie einen YubiKey um auf Ihr Konto zuzugreifen. Funtioniert mit YubiKey 4, Nano 4, 4C und NEO Geräten." + "message": "Verwende einen YubiKey 4, 5 oder NEO." }, "duoDescV2": { - "message": "Enter a code generated by Duo Security.", + "message": "Gib einen von Duo Security generierten Code ein.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -1003,13 +1003,13 @@ "message": "Benutze einen FIDO U2F-kompatiblen Sicherheitsschlüssel, um auf dein Konto zuzugreifen." }, "u2fTitle": { - "message": "FIDO U2F Sicherheitsschlüssel" + "message": "FIDO U2F-Sicherheitsschlüssel" }, "webAuthnTitle": { - "message": "FIDO2 WebAuthn" + "message": "Passkey" }, "webAuthnDesc": { - "message": "Benutze einen WebAuthn-kompatiblen Sicherheitsschlüssel, um auf dein Konto zuzugreifen." + "message": "Verwende die Biometrie deines Gerätes oder einen FIDO2-kompatiblen Sicherheitsschlüssel." }, "webAuthnMigrated": { "message": "(Von FIDO migriert)" @@ -1018,7 +1018,7 @@ "message": "E-Mail" }, "emailDescV2": { - "message": "Enter a code sent to your email." + "message": "Gib einen an deine E-Mail-Adresse gesendeten Code ein." }, "continue": { "message": "Fortsetzen" @@ -1060,7 +1060,7 @@ "message": "Bist du sicher, dass du fortfahren möchtest?" }, "moveSelectedItemsDesc": { - "message": "Wählen Sie einen Ordner aus, in den Sie $COUNT$ ausgewählte(s) Objekt(e) verschieben möchten.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1659,7 +1659,7 @@ "message": "Geben Sie Ihr Master-Passwort ein, um die Zwei-Faktor-Anmeldeeinstellungen zu ändern." }, "twoStepAuthenticatorInstructionPrefix": { - "message": "Download an authenticator app such as" + "message": "Lade eine Authentifizierungs-App herunter, wie z.B." }, "twoStepAuthenticatorInstructionInfix1": { "message": "," @@ -1680,16 +1680,19 @@ } }, "continueToExternalUrlDesc": { - "message": "You are leaving Bitwarden and launching an external website in a new window." + "message": "Du verlässt Bitwarden und öffnest eine externe Website in einem neuen Fenster." }, "twoStepContinueToBitwardenUrlTitle": { "message": "Weiter zu bitwarden.com?" }, "twoStepContinueToBitwardenUrlDesc": { - "message": "Bitwarden Authenticator allows you to store authenticator keys and generate TOTP codes for 2-step verification flows. Learn more on the bitwarden.com website." + "message": "Mit dem Bitwarden Authenticator kannst du Authentifizierungsschlüssel speichern und TOTP-Codes für Zwei-Faktor-Authentifizierungsprozesse generieren. Erfahre mehr auf der bitwarden.com Website." }, "twoStepAuthenticatorScanCodeV2": { - "message": "Scan the QR code below with your authenticator app or enter the key." + "message": "Scanne den QR-Code unten mit deiner Authentifizierungs-App oder gib den Schlüssel ein." + }, + "twoStepAuthenticatorQRCanvasError": { + "message": "QR-Code konnte nicht geladen werden. Versuche es erneut oder verwende den Schlüssel unten." }, "key": { "message": "Schlüssel" @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "Deine Konto-E-Mail-Adresse wurde verifiziert" }, + "emailVerifiedV2": { + "message": "E-Mail-Adresse verifiziert" + }, "emailVerifiedFailed": { "message": "Ihre E-Mail kann nicht verifiziert werden. Versuchen Sie eine neue Bestätigungs-E-Mail zu senden." }, @@ -3706,7 +3712,7 @@ } }, "subscriptionSeatMaxReached": { - "message": "You cannot invite more than $COUNT$ members without increasing your subscription seats.", + "message": "Du kannst nicht mehr als $COUNT$ Mitglieder einladen, ohne deine Benutzerplätze zu erhöhen.", "placeholders": { "count": { "content": "$1", @@ -3787,7 +3793,7 @@ "message": "jederzeit." }, "byContinuingYouAgreeToThe": { - "message": "Indem Sie fortfahren, stimmen Sie unseren" + "message": "Indem du fortfährst, stimmst du den" }, "and": { "message": "und" @@ -8439,6 +8445,9 @@ "memberAccessReportDesc": { "message": "Stelle sicher, dass Mitglieder Zugriff auf die richtigen Zugangsdaten haben und ihre Konten sicher sind. Benutze diesen Bericht, um eine CSV-Datei mit Mitgliederzugriffen und Kontokonfigurationen zu erhalten." }, + "memberAccessReportPageDesc": { + "message": "Kontrolliere den Zugriff von Organisationsmitgliedern auf Gruppen, Sammlungen und Sammlungseinträgen. Der CSV-Export bietet eine detaillierte Aufschlüsselung pro Mitglied, einschließlich Informationen über Sammlungsberechtigungen und Kontenkonfigurationen." + }, "higherKDFIterations": { "message": "Höhere KDF-Iterationen können helfen, dein Master-Passwort vor Brute-Force-Attacken durch einen Angreifer zu schützen." }, @@ -8509,20 +8518,62 @@ "message": "Client details" }, "downloadCSV": { - "message": "CSV herunterladen" + "message": "CSV-Datei herunterladen" }, "monthlySubscriptionUserSeatsMessage": { - "message": "Adjustments to your subscription will result in prorated charges to your billing totals on your next billing period. " + "message": "Anpassungen an deinem Abonnement führen zu einer anteiligen Erhöhung deines Rechnungsbetrags im nächsten Abrechnungszeitraum. " }, "annualSubscriptionUserSeatsMessage": { - "message": "Adjustments to your subscription will result in prorated charges on a monthly billing cycle. " + "message": "Anpassungen an deinem Abonnement führen zu einer anteiligen Erhöhung in deinem monatlichen Abrechnungszeitraum. " }, "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": "Lade eine CSV-Datei herunter, um Kundendetails für jedes Rechnungsdatum zu erhalten. Anteilige Kosten sind nicht in der CSV-Datei enthalten und können von der zugehörigen Rechnung abweichen. Detaillierte Angaben zur Rechnungsstellung findest du in deinen monatlichen Abrechnungen.", "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": { - "message": "There are no invoices to list", + "message": "Es gibt keine Rechnungen zum Anzeigen", "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": "Hinweis: Später in diesem Monat wird die Kundentresor-Sicherheit verbessert und Mitglieder eines Anbieters haben dann keinen direkten Zugriff mehr auf Einträge des Kundentresors. Bei Fragen", + "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": { + "message": "kontaktiere den Bitwarden Support.", + "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'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Gesponsert" + }, + "licenseAndBillingManagementDesc": { + "message": "Lade nach der Durchführung von Aktualisierungen im Bitwarden Cloud Server deine Lizenzdatei hoch, um die neuesten Änderungen anzuwenden." + }, + "addToFolder": { + "message": "Add to folder" + }, + "selectFolder": { + "message": "Select folder" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/el/messages.json b/apps/web/src/locales/el/messages.json index 96c092051a5..a33b71b1917 100644 --- a/apps/web/src/locales/el/messages.json +++ b/apps/web/src/locales/el/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "Διεύθυνση Email" }, - "yourVaultIsLocked": { - "message": "Το vault σας είναι κλειδωμένο. Επαληθεύστε τον κύριο κωδικό πρόσβασης για να συνεχίσετε." + "yourVaultIsLockedV2": { + "message": "Your vault is locked" }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Είστε βέβαιοι ότι θέλετε να συνεχίσετε;" }, "moveSelectedItemsDesc": { - "message": "Επιλέξτε ένα φάκελο στον οποίο θέλετε να μετακινήσετε το $COUNT$ επιλεγμένο(α) στοιχεία.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Scan the QR code below with your authenticator app or enter the key." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Could not load QR code. Try again or use the key below." + }, "key": { "message": "Κλειδί" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "Το email σας έχει επαληθευτεί." }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerifiedFailed": { "message": "Δεν είναι δυνατή η επαλήθευση του email σας. Δοκιμάστε να στείλετε νέο email επαλήθευσης." }, @@ -7877,7 +7883,7 @@ "message": "Assign to these collections" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Select collections to assign" @@ -8439,6 +8445,9 @@ "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." }, + "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." + }, "higherKDFIterations": { "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." }, @@ -8524,5 +8533,47 @@ "noInvoicesToList": { "message": "There are no invoices to list", "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,", + "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": { + "message": "contact Bitwarden support.", + "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'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsored" + }, + "licenseAndBillingManagementDesc": { + "message": "After making updates in the Bitwarden cloud server, upload your license file to apply the most recent changes." + }, + "addToFolder": { + "message": "Add to folder" + }, + "selectFolder": { + "message": "Select folder" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/en_GB/messages.json b/apps/web/src/locales/en_GB/messages.json index c6ad1dee113..a6036f687c1 100644 --- a/apps/web/src/locales/en_GB/messages.json +++ b/apps/web/src/locales/en_GB/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "Email address" }, - "yourVaultIsLocked": { - "message": "Your vault is locked. Verify your master password to continue." + "yourVaultIsLockedV2": { + "message": "Your vault is locked." }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Are you sure you want to continue?" }, "moveSelectedItemsDesc": { - "message": "Choose a folder that you would like to move the $COUNT$ selected item(s) to.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Scan the QR code below with your authenticator app or enter the key." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Could not load QR code. Try again or use the key below." + }, "key": { "message": "Key" }, @@ -2420,7 +2423,7 @@ "message": "Licence file" }, "licenseFileDesc": { - "message": "Your license file will be named something like $FILE_NAME$", + "message": "Your licence file will be named something like $FILE_NAME$", "placeholders": { "file_name": { "content": "$1", @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "Account email verified" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerifiedFailed": { "message": "Unable to verify your email. Try sending a new verification email." }, @@ -7877,7 +7883,7 @@ "message": "Assign to these collections" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organisation members with access to these collections will be able to see the items." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Select collections to assign" @@ -8439,6 +8445,9 @@ "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." }, + "memberAccessReportPageDesc": { + "message": "Audit organisation 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." + }, "higherKDFIterations": { "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." }, @@ -8524,5 +8533,47 @@ "noInvoicesToList": { "message": "There are no invoices to list", "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,", + "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": { + "message": "contact Bitwarden support.", + "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'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsored" + }, + "licenseAndBillingManagementDesc": { + "message": "After making updates in the Bitwarden cloud server, upload your licence file to apply the most recent changes." + }, + "addToFolder": { + "message": "Add to folder" + }, + "selectFolder": { + "message": "Select folder" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organisation. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/en_IN/messages.json b/apps/web/src/locales/en_IN/messages.json index 3fc204d5d40..38fd6bdc5f7 100644 --- a/apps/web/src/locales/en_IN/messages.json +++ b/apps/web/src/locales/en_IN/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "Email address" }, - "yourVaultIsLocked": { - "message": "Your vault is locked. Verify your master password to continue." + "yourVaultIsLockedV2": { + "message": "Your vault is locked." }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Are you sure you want to continue?" }, "moveSelectedItemsDesc": { - "message": "Choose a folder that you would like to move the $COUNT$ selected item(s) to.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Scan the QR code below with your authenticator app or enter the key." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Could not load QR code. Try again or use the key below." + }, "key": { "message": "Key" }, @@ -2420,7 +2423,7 @@ "message": "Licence file" }, "licenseFileDesc": { - "message": "Your license file will be named something like $FILE_NAME$", + "message": "Your licence file will be named something like $FILE_NAME$", "placeholders": { "file_name": { "content": "$1", @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "Your email has been verified." }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerifiedFailed": { "message": "Unable to verify your email. Try sending a new verification email." }, @@ -7877,7 +7883,7 @@ "message": "Assign to these collections" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organisation members with access to these collections will be able to see the items." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Select collections to assign" @@ -8439,6 +8445,9 @@ "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." }, + "memberAccessReportPageDesc": { + "message": "Audit organisation 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." + }, "higherKDFIterations": { "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." }, @@ -8524,5 +8533,47 @@ "noInvoicesToList": { "message": "There are no invoices to list", "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,", + "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": { + "message": "contact Bitwarden support.", + "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'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsored" + }, + "licenseAndBillingManagementDesc": { + "message": "After making updates in the Bitwarden cloud server, upload your licence file to apply the most recent changes." + }, + "addToFolder": { + "message": "Add to folder" + }, + "selectFolder": { + "message": "Select folder" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organisation. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/eo/messages.json b/apps/web/src/locales/eo/messages.json index e618e648545..b84133ad0df 100644 --- a/apps/web/src/locales/eo/messages.json +++ b/apps/web/src/locales/eo/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "Retpoŝta Adreso" }, - "yourVaultIsLocked": { - "message": "Via trezorejo estas ŝlosita. Kontrolu vian ĉefan pasvorton por daŭrigi." + "yourVaultIsLockedV2": { + "message": "Your vault is locked" }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Are you sure you want to continue?" }, "moveSelectedItemsDesc": { - "message": "Elektu dosierujon al kiu vi ŝatus movi la elektitajn erojn $COUNT$.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Scan the QR code below with your authenticator app or enter the key." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Could not load QR code. Try again or use the key below." + }, "key": { "message": "Konigilo" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "Via retpoŝto estis kontrolita." }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerifiedFailed": { "message": "Ne eblas kontroli vian retpoŝton. Provu sendi novan kontrolan retpoŝton." }, @@ -7877,7 +7883,7 @@ "message": "Assign to these collections" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Select collections to assign" @@ -8439,6 +8445,9 @@ "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." }, + "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." + }, "higherKDFIterations": { "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." }, @@ -8524,5 +8533,47 @@ "noInvoicesToList": { "message": "There are no invoices to list", "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,", + "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": { + "message": "contact Bitwarden support.", + "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'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsored" + }, + "licenseAndBillingManagementDesc": { + "message": "After making updates in the Bitwarden cloud server, upload your license file to apply the most recent changes." + }, + "addToFolder": { + "message": "Add to folder" + }, + "selectFolder": { + "message": "Select folder" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/es/messages.json b/apps/web/src/locales/es/messages.json index 098272168a7..cc52cf68df2 100644 --- a/apps/web/src/locales/es/messages.json +++ b/apps/web/src/locales/es/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "Correo electrónico" }, - "yourVaultIsLocked": { - "message": "Tu caja fuerte está bloqueada. Verifica tu contraseña maestra para continuar." + "yourVaultIsLockedV2": { + "message": "Your vault is locked" }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "¿Está seguro de que desea continuar?" }, "moveSelectedItemsDesc": { - "message": "Selecciona una carpeta a la que quieras mover los $COUNT$ elementos seleccionados.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Scan the QR code below with your authenticator app or enter the key." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Could not load QR code. Try again or use the key below." + }, "key": { "message": "Clave" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "Tu cuenta de correo ha sido verificada." }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerifiedFailed": { "message": "No se ha podido verificar tu cuenta de correo electrónico. Prueba a enviar un nuevo correo de verificación." }, @@ -7877,7 +7883,7 @@ "message": "Asignar a estas colecciones" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Seleccionar colecciones para asignar" @@ -8439,6 +8445,9 @@ "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." }, + "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." + }, "higherKDFIterations": { "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." }, @@ -8524,5 +8533,47 @@ "noInvoicesToList": { "message": "There are no invoices to list", "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,", + "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": { + "message": "contact Bitwarden support.", + "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'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsored" + }, + "licenseAndBillingManagementDesc": { + "message": "After making updates in the Bitwarden cloud server, upload your license file to apply the most recent changes." + }, + "addToFolder": { + "message": "Add to folder" + }, + "selectFolder": { + "message": "Select folder" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/et/messages.json b/apps/web/src/locales/et/messages.json index 881e5d62c7b..aa27b64bc8b 100644 --- a/apps/web/src/locales/et/messages.json +++ b/apps/web/src/locales/et/messages.json @@ -174,7 +174,7 @@ "description": "This is the folder for uncategorized items" }, "selfOwnershipLabel": { - "message": "You", + "message": "Sina", "description": "Used as a label to indicate that the user is the owner of an item." }, "addFolder": { @@ -406,10 +406,10 @@ "message": "Kirje" }, "itemDetails": { - "message": "Item details" + "message": "Vaata detaile" }, "itemName": { - "message": "Item name" + "message": "Kirje nimi" }, "cannotRemoveViewOnlyCollections": { "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", @@ -598,16 +598,16 @@ "message": "Juurdepääs" }, "accessLevel": { - "message": "Access level" + "message": "Juurdepääsu aste" }, "accessing": { - "message": "Accessing" + "message": "Hangin juurdepääsu" }, "loggedOut": { "message": "Välja logitud" }, "loggedOutDesc": { - "message": "You have been logged out of your account." + "message": "Sa logisid oma kontolt välja." }, "loginExpired": { "message": "Sessioon on aegunud." @@ -631,82 +631,82 @@ "message": "Logi sisse või loo uus konto." }, "loginWithDevice": { - "message": "Log in with device" + "message": "Logi sisse teise seadmega" }, "loginWithDeviceEnabledNote": { - "message": "Log in with device must be set up in the settings of the Bitwarden app. Need another option?" + "message": "Bitwardeni rakenduse seadistuses peab olema konfigureeritud sisselogimine läbi seadme. Soovid teist valikut?" }, "loginWithMasterPassword": { "message": "Logi sisse ülemparooliga" }, "readingPasskeyLoading": { - "message": "Reading passkey..." + "message": "Loen pääsuvõtit..." }, "readingPasskeyLoadingInfo": { - "message": "Keep this window open and follow prompts from your browser." + "message": "Hoia see aken lahti ja järgi brauseri juhiseid." }, "useADifferentLogInMethod": { - "message": "Use a different log in method" + "message": "Kasuta teist logimismeetodit" }, "loginWithPasskey": { - "message": "Log in with passkey" + "message": "Logi sisse pääsuvõtmega" }, "invalidPasskeyPleaseTryAgain": { - "message": "Invalid Passkey. Please try again." + "message": "Vigane pääsuvõti. Palun proovi uuesti." }, "twoFactorForPasskeysNotSupportedOnClientUpdateToLogIn": { - "message": "2FA for passkeys is not supported. Update the app to log in." + "message": "2FA pääsuvõtmed pole toetatud. Uuenda sisse logimiseks rakendust." }, "loginWithPasskeyInfo": { "message": "Use a generated passkey that will automatically log you in without a password. Biometrics, like facial recognition or fingerprint, or another FIDO2 security method will verify your identity." }, "newPasskey": { - "message": "New passkey" + "message": "Uus pääsukood" }, "learnMoreAboutPasswordless": { - "message": "Learn more about passwordless" + "message": "Uuri lähemalt nullparooli kohta" }, "creatingPasskeyLoading": { - "message": "Creating passkey..." + "message": "Loon pääsuvõtit..." }, "creatingPasskeyLoadingInfo": { - "message": "Keep this window open and follow prompts from your browser." + "message": "Hoia see aken lahti ja järgi brauseri juhiseid." }, "errorCreatingPasskey": { - "message": "Error creating passkey" + "message": "Pääsuvõtme loomine ebaõnnestus" }, "errorCreatingPasskeyInfo": { - "message": "There was a problem creating your passkey." + "message": "Tekkis probleem pääsuvõtme loomisel." }, "passkeySuccessfullyCreated": { - "message": "Passkey successfully created!" + "message": "Pääsuvõti edukalt loodud!" }, "customPasskeyNameInfo": { - "message": "Name your passkey to help you identify it." + "message": "Anna oma pääsuvõtmele nimi, et seda hiljem lihtsamini tuvastada." }, "useForVaultEncryption": { - "message": "Use for vault encryption" + "message": "Kasuta hoidla krüpteerimiseks" }, "useForVaultEncryptionInfo": { "message": "Log in and unlock on supported devices without your master password. Follow the prompts from your browser to finalize setup." }, "useForVaultEncryptionErrorReadingPasskey": { - "message": "Error reading passkey. Try again or uncheck this option." + "message": "Ei õnnestunud pääsuvõtit lugeda. Proovi uuesti või tühjendage see valik." }, "encryptionNotSupported": { - "message": "Encryption not supported" + "message": "Krüpteerimine ei ole toetatud" }, "enablePasskeyEncryption": { - "message": "Set up encryption" + "message": "Seadista krüpteerimine" }, "usedForEncryption": { "message": "Used for encryption" }, "loginWithPasskeyEnabled": { - "message": "Log in with passkey turned on" + "message": "Logi sisse pääsuvõti sisse lülitatult" }, "passkeySaved": { - "message": "$NAME$ saved", + "message": "$NAME$ salvestatud", "placeholders": { "name": { "content": "$1", @@ -715,31 +715,31 @@ } }, "passkeyRemoved": { - "message": "Passkey removed" + "message": "Pääsuvõti eemaldatud" }, "removePasskey": { - "message": "Remove passkey" + "message": "Eemalda pääsuvõti" }, "removePasskeyInfo": { - "message": "If all passkeys are removed, you will be unable to log into new devices without your master password." + "message": "Kui kõik pääsuvõtmed on eemaldatud, ei saa sa sisse logida uutesse seadmetesse ilma ülemparoolita." }, "passkeyLimitReachedInfo": { - "message": "Passkey limit reached. Remove a passkey to add another." + "message": "Pääsuvõtmete limiit täitus. Eemalda mõni, et uusi lisada." }, "tryAgain": { - "message": "Try again" + "message": "Proovi uuesti" }, "createAccount": { "message": "Konto loomine" }, "setAStrongPassword": { - "message": "Set a strong password" + "message": "Määra tugev parool" }, "finishCreatingYourAccountBySettingAPassword": { - "message": "Finish creating your account by setting a password" + "message": "Lõpeta konto loomine määrates parooli" }, "newAroundHere": { - "message": "New around here?" + "message": "Kas oled uus?" }, "startTrial": { "message": "Alusta prooviperioodi" @@ -748,10 +748,10 @@ "message": "Logi sisse" }, "verifyIdentity": { - "message": "Verify your Identity" + "message": "Kinnitage oma Identiteet" }, "logInInitiated": { - "message": "Log in initiated" + "message": "Sisselogimine käivitatud" }, "submit": { "message": "Kinnita" @@ -787,7 +787,7 @@ "message": "Ülemparooli vihje" }, "masterPassHintText": { - "message": "If you forget your password, the password hint can be sent to your email. $CURRENT$/$MAXIMUM$ character maximum.", + "message": "Kui sa unustad oma parooli, saad saata parooli vihje e-mailile.\n$CURRENT$/$MAXIMUM$ tähepiirang.", "placeholders": { "current": { "content": "$1", @@ -824,7 +824,7 @@ "message": "Vajalik on ülemparooli uuesti sisestamine." }, "masterPasswordMinlength": { - "message": "Master password must be at least $VALUE$ characters long.", + "message": "Ülemparool peab olema vähemalt $VALUE$ märki pikk.", "description": "The Master Password must be at least a specific number of characters long.", "placeholders": { "value": { @@ -849,13 +849,13 @@ "message": "Tekkis ootamatu viga." }, "expirationDateError": { - "message": "Please select an expiration date that is in the future." + "message": "Palun vali kehtivuse lõppemise tähtaeg tulevikust." }, "emailAddress": { "message": "E-posti aadress" }, - "yourVaultIsLocked": { - "message": "Hoidla on lukus. Jätkamiseks sisesta ülemparool." + "yourVaultIsLockedV2": { + "message": "Sinu hoidla on lukus." }, "uuid": { "message": "UUID" @@ -880,7 +880,7 @@ "message": "Vale ülemparool" }, "invalidFilePassword": { - "message": "Invalid file password, please use the password you entered when you created the export file." + "message": "Vale parool, palun kasuta seda parooli mille sisestasid eksport faili loomisel." }, "lockNow": { "message": "Lukusta paroolihoidla" @@ -889,7 +889,7 @@ "message": "Puuduvad kirjed, mida kuvada." }, "noPermissionToViewAllCollectionItems": { - "message": "You do not have permission to view all items in this collection." + "message": "Sul ei ole õigust vaadata kõiki asju selles kogus." }, "noCollectionsInList": { "message": "Puuduvad kollektsioonid, mida kuvada." @@ -901,7 +901,7 @@ "message": "Puuduvad kasutajad, keda kuvada." }, "noMembersInList": { - "message": "There are no members to list." + "message": "Puuduvad kasutajad, keda kuvada." }, "noEventsInList": { "message": "Puuduvad sündmused, mida kuvada." @@ -1060,7 +1060,7 @@ "message": "Are you sure you want to continue?" }, "moveSelectedItemsDesc": { - "message": "Vali kaust, kuhu soovid need $COUNT$ kirjet liigutada.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1493,10 +1493,10 @@ "message": "Vali imporditav fail" }, "chooseFile": { - "message": "Choose File" + "message": "Vali fail" }, "noFileChosen": { - "message": "No file chosen" + "message": "Ühtegi faili pole valitud" }, "orCopyPasteFileContents": { "message": "või kopeeri/kleebi imporditava faili sisu" @@ -1581,26 +1581,26 @@ "message": "Kaheastmeline kinnitamine" }, "twoStepLoginEnforcement": { - "message": "Two-step Login Enforcement" + "message": "Kaheastmelise Logimise Jõustamine" }, "twoStepLoginDesc": { "message": "Kaitse oma kontot, nõudes sisselogimisel lisakinnitust." }, "twoStepLoginTeamsDesc": { - "message": "Enable two-step login for your organization." + "message": "Luba kaheastmeline logimine enda organisatsioonis." }, "twoStepLoginEnterpriseDescStart": { - "message": "Enforce Bitwarden Two-step Login options for members by using the ", + "message": "Jõusta Bitwardeni Kaheastmelise Logimise valikud liikmetele kasutades ", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Enforce Bitwarden Two-step Login options for members by using the Two-step Login Policy.'" }, "twoStepLoginPolicy": { - "message": "Two-step Login Policy" + "message": "Kaheastmelise Sisselogimise Reeglid" }, "twoStepLoginOrganizationDuoDesc": { - "message": "To enforce Two-step Login through Duo, use the options below." + "message": "Et jõustada Kaheastmeline Logimine läbi Duo, kasuta allolevaid valikuid." }, "twoStepLoginOrganizationSsoDesc": { - "message": "If you have setup SSO or plan to, Two-step Login may already be enforced through your Identity Provider." + "message": "Kui sa oled seadistanud SSO või plaanid seda, Kaheastmeline Logimine võib juba olla jõustatud läbi sinu Identiteedi Pakkuja." }, "twoStepLoginRecoveryWarning": { "message": "Kaheastmelise kinnitamine aktiveerimine võib luua olukorra, kus sul on võimatu oma Bitwardeni kontosse sisse logida. Näiteks kui kaotad oma nutiseadme. Taastamise kood võimaldab aga kontole ligi pääseda ka olukorras, kus kaheastmelist kinnitamist ei ole võimalik läbi viia. Sellistel juhtudel ei saa ka Bitwardeni klienditugi sinu kontole ligipääsu taastada. Selle tõttu soovitame taastekoodi välja printida ja seda turvalises kohas hoida." @@ -1659,19 +1659,19 @@ "message": "Kaheastmelise kinnitamise seadete muutmiseks pead sisestama ülemparooli." }, "twoStepAuthenticatorInstructionPrefix": { - "message": "Download an authenticator app such as" + "message": "Lae alla autentiteerimise rakendus nagu" }, "twoStepAuthenticatorInstructionInfix1": { "message": "," }, "twoStepAuthenticatorInstructionInfix2": { - "message": "or" + "message": "või" }, "twoStepAuthenticatorInstructionSuffix": { "message": "." }, "continueToExternalUrlTitle": { - "message": "Continue to $URL$?", + "message": "Mine edasi $URL$-i?", "placeholders": { "url": { "content": "$1", @@ -1680,22 +1680,25 @@ } }, "continueToExternalUrlDesc": { - "message": "You are leaving Bitwarden and launching an external website in a new window." + "message": "Oled lahkumas Bitwardenist ja avamas välist veebilehte uues aknas." }, "twoStepContinueToBitwardenUrlTitle": { - "message": "Continue to bitwarden.com?" + "message": "Mine edasi bitwarden.com-i?" }, "twoStepContinueToBitwardenUrlDesc": { - "message": "Bitwarden Authenticator allows you to store authenticator keys and generate TOTP codes for 2-step verification flows. Learn more on the bitwarden.com website." + "message": "Bitwardeni Autentiteerijaga saad sa hoiustada autentiteerimise võtmeid ja luua TOTP koode kaheastmeliseks kinnitamiseks. Uuri lähemalt bitwarden.com veebilehelt." }, "twoStepAuthenticatorScanCodeV2": { - "message": "Scan the QR code below with your authenticator app or enter the key." + "message": "Skänneeri allolev QR-kood oma autentiteerimisrakendusega või sisesta kood." + }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Ei õnnestunud laadida QR-koodi. Proovi uuesti või kasuta allolevat koodi." }, "key": { "message": "Võti" }, "twoStepAuthenticatorEnterCodeV2": { - "message": "Verification code" + "message": "Kinnituskood" }, "twoStepAuthenticatorReaddDesc": { "message": "Kui soovid lisada veel seadmeid, siis all on kuvatud QR kood (ehk võti), mida autentimisrakendusega kasutada saad." @@ -1776,10 +1779,10 @@ "message": "Sisesta oma Bitwardeni rakenduse informatsioon Duo admini paneelist." }, "twoFactorDuoClientId": { - "message": "Client Id" + "message": "Kliendi Id" }, "twoFactorDuoClientSecret": { - "message": "Client Secret" + "message": "Kliendi Saladus" }, "twoFactorDuoApiHostname": { "message": "API hostinimi" @@ -1862,7 +1865,7 @@ "description": "Vault health reports can be used to evaluate the security of your Bitwarden individual or organization vault." }, "orgsReportsDesc": { - "message": "Identify and close security gaps in your organization's accounts by clicking the reports below.", + "message": "Idenfitseeri ja paranda turvaprobleeme oma organisatsiooni kontodes vajutades allolevatele raportidele.", "description": "Vault health reports can be used to evaluate the security of your Bitwarden individual or organization vault." }, "unsecuredWebsitesReport": { @@ -1875,7 +1878,7 @@ "message": "Leiti ebaturvalisi veebilehti" }, "unsecuredWebsitesFoundReportDesc": { - "message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", + "message": "Leidsime sinu hoidla(st/test) $COUNT$ eset ebaturvaliste URI-dega. Peaksid muutma neid, kui su sait lubab, https:// -iks.", "placeholders": { "count": { "content": "$1", @@ -1900,7 +1903,7 @@ "message": "Kaheastmelise kinnituseta kontod" }, "inactive2faFoundReportDesc": { - "message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", + "message": "Me leitsime $COUNT$ veebilehe(veebilehte) sinu $VAULT$ mis ei pruugi olla seadistatud kaheastmelise logimisega (2fa andmebaasi järgi). Et oma kontosid kaitsta, soovitame tungivalt sisse seada kaheastmelise sisselogimise.", "placeholders": { "count": { "content": "$1", @@ -1928,7 +1931,7 @@ "message": "Avastatud on lekkinud paroole" }, "exposedPasswordsFoundReportDesc": { - "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", + "message": "Leidsime $COUNT$ eseme(eset) sinu $VAULT$ mille parool paljastati hiljutiste andmelekete käigus. Soovitame tungivalt muuta kohe need paroolid.", "placeholders": { "count": { "content": "$1", @@ -1965,7 +1968,7 @@ "message": "Avastatud on nõrgad paroolid" }, "weakPasswordsFoundReportDesc": { - "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", + "message": "Me leidsime $COUNT$ eset sinu $VAULT$ nõrkade paroolidega. Sa peaksid vahetama need tugevamate vastu.", "placeholders": { "count": { "content": "$1", @@ -1990,7 +1993,7 @@ "message": "Leiti korduvalt kasutatud paroole" }, "reusedPasswordsFoundReportDesc": { - "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.", + "message": "Me leidsime $COUNT$ parooli, mida on korduvalt kasutatud sinu $VAULT$. Sa peaksid korduvad paroolid välja vahetama ainulaadsete vastu.", "placeholders": { "count": { "content": "$1", @@ -2148,7 +2151,7 @@ } }, "premiumPriceWithFamilyPlan": { - "message": "Go premium for just $PRICE$ /year, or get premium accounts for $FAMILYPLANUSERCOUNT$ users and unlimited family sharing with a ", + "message": "Hangi preemiumi ainult $PRICE$ eest aastas või hangi preemium $FAMILYPLANUSERCOUNT$ kasutajale pluss piiramatu peresisene jagamine ", "placeholders": { "price": { "content": "$1", @@ -2161,7 +2164,7 @@ } }, "bitwardenFamiliesPlan": { - "message": "Bitwarden Families plan." + "message": "Bitwardeni Pereplaaniga." }, "addons": { "message": "Lisad" @@ -2237,7 +2240,7 @@ } }, "paymentChargedWithUnpaidSubscription": { - "message": "Your payment method will be charged for any unpaid subscriptions." + "message": "Sinu maksemeetodit kasutatakse maksmata tellimuste tasumiseks." }, "paymentChargedWithTrial": { "message": "Valitud pakett sisaldab 7 päevast prooviperioodi. Krediitkaardilt ei võeta raha enne, kui prooviperiood läbi saab. Väljatoodud summa debiteeritakse iga $INTERVAL$. Tellimust on võimalik igal ajal tühistada." @@ -2261,7 +2264,7 @@ "message": "Tühista tellimus" }, "subscriptionExpiration": { - "message": "Subscription expiration" + "message": "Tellimuse lõppemine" }, "subscriptionCanceled": { "message": "Tellimus on tühistatud." @@ -2408,7 +2411,7 @@ "message": "Võta klienditoega ühendust" }, "contactSupportShort": { - "message": "Contact Support" + "message": "Võta kasutajatoega ühendust" }, "updatedPaymentMethod": { "message": "Maksemeetod on muudetud." @@ -2626,7 +2629,7 @@ } }, "trialSecretsManagerThankYou": { - "message": "Thanks for signing up for Bitwarden Secrets Manager for $PLAN$!", + "message": "Aitäh soetamast Bitwarden Secrets Manageri $PLAN$ tellimusega!", "placeholders": { "plan": { "content": "$1", @@ -2719,7 +2722,7 @@ "message": "Tahad kindlasti selle grupi kustutada?" }, "deleteMultipleGroupsConfirmation": { - "message": "Are you sure you want to delete the following $QUANTITY$ group(s)?", + "message": "Kas sa oled kindel, et tahad kustudada $QUANTITY$ gruppi?", "placeholders": { "quantity": { "content": "$1", @@ -2737,7 +2740,7 @@ "message": "Kui kasutaja ligipääsu luba on tühistatud, ei saa ta enam origanisatsiooni andmetele ligi. Ligipääsu taastamiseks ava \"Tühistatud\" kaart." }, "removeUserConfirmationKeyConnector": { - "message": "Warning! This user requires Key Connector to manage their encryption. Removing this user from your organization will permanently deactivate their account. This action cannot be undone. Do you want to proceed?" + "message": "Hoiatus! See kasutaja vajab Key Connectorit oma krüpteeringu haldamiseks. Tema eemaldamine organisatsioonist deaktiveerib jäädavalt tema konto. Seda otsust ei saa muuta. Oled kindel?" }, "externalId": { "message": "Väline ID" @@ -2776,7 +2779,7 @@ "message": "Oled kindel, et soovid selle kogumiku kustutada?" }, "editMember": { - "message": "Edit member" + "message": "Muuda liiget" }, "fieldOnTabRequiresAttention": { "message": "A field on the '$TAB$' tab requires your attention.", @@ -2845,7 +2848,7 @@ "message": "Kõik" }, "addAccess": { - "message": "Add Access" + "message": "Anna Juurdepääs" }, "addAccessFilter": { "message": "Add Access Filter" @@ -2884,7 +2887,7 @@ "message": "CLI" }, "bitWebVault": { - "message": "Bitwarden Web vault" + "message": "Bitwarden Web Vault" }, "bitSecretsManager": { "message": "Bitwarden Secrets Manager" @@ -2911,10 +2914,10 @@ "message": "Sisselogimine nurjus vale kaheastmelise kinnituse tõttu." }, "incorrectPassword": { - "message": "Incorrect password" + "message": "Vale parool" }, "incorrectCode": { - "message": "Incorrect code" + "message": "Vale kood" }, "incorrectPin": { "message": "Incorrect PIN" @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "E-posti aadress on kinnitatud." }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerifiedFailed": { "message": "E-posti kinnitamine nurjus. Proovi uut kinnituskirja saata." }, @@ -3781,16 +3787,16 @@ "message": "Get emails from Bitwarden for announcements, advice, and research opportunities." }, "unsubscribe": { - "message": "Unsubscribe" + "message": "Lõpeta tellimus" }, "atAnyTime": { - "message": "at any time." + "message": "iga hetk." }, "byContinuingYouAgreeToThe": { - "message": "By continuing, you agree to the" + "message": "Jätkates nõustud" }, "and": { - "message": "and" + "message": "ja" }, "acceptPolicies": { "message": "Märkeruudu markeerimisel nõustud järgnevaga:" @@ -5505,16 +5511,16 @@ "message": "Kood on saadetud" }, "verificationCode": { - "message": "Verification code" + "message": "Kinnituskood" }, "confirmIdentity": { "message": "Jätkamiseks kinnita oma identiteet." }, "verificationCodeRequired": { - "message": "Verification code is required." + "message": "Kinnituskood on nõutav." }, "invalidVerificationCode": { - "message": "Invalid verification code" + "message": "Vale kinnituskood" }, "convertOrganizationEncryptionDesc": { "message": "$ORGANIZATION$ is using SSO with a self-hosted key server. A master password is no longer required to log in for members of this organization.", @@ -7877,7 +7883,7 @@ "message": "Assign to these collections" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Select collections to assign" @@ -8439,6 +8445,9 @@ "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." }, + "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." + }, "higherKDFIterations": { "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." }, @@ -8524,5 +8533,47 @@ "noInvoicesToList": { "message": "There are no invoices to list", "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,", + "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": { + "message": "contact Bitwarden support.", + "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'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsored" + }, + "licenseAndBillingManagementDesc": { + "message": "After making updates in the Bitwarden cloud server, upload your license file to apply the most recent changes." + }, + "addToFolder": { + "message": "Add to folder" + }, + "selectFolder": { + "message": "Select folder" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/eu/messages.json b/apps/web/src/locales/eu/messages.json index 0edc6719686..e8713254735 100644 --- a/apps/web/src/locales/eu/messages.json +++ b/apps/web/src/locales/eu/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "Emaila" }, - "yourVaultIsLocked": { - "message": "Zure kutxa gotorra blokeatuta dago. Egiaztatu zure pasahitz nagusia jarraitzeko." + "yourVaultIsLockedV2": { + "message": "Your vault is locked" }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Are you sure you want to continue?" }, "moveSelectedItemsDesc": { - "message": "Aukeratu $COUNT$ hautatutako artikulu mugitu nahi d(it)uzun karpeta.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Scan the QR code below with your authenticator app or enter the key." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Could not load QR code. Try again or use the key below." + }, "key": { "message": "Gakoa" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "Zure emaila egiaztatu da." }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerifiedFailed": { "message": "Ezin izan da emaila egiaztatu. Saiatu egiaztatzeko email berri bat bidaltzen." }, @@ -7877,7 +7883,7 @@ "message": "Assign to these collections" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Select collections to assign" @@ -8439,6 +8445,9 @@ "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." }, + "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." + }, "higherKDFIterations": { "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." }, @@ -8524,5 +8533,47 @@ "noInvoicesToList": { "message": "There are no invoices to list", "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,", + "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": { + "message": "contact Bitwarden support.", + "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'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsored" + }, + "licenseAndBillingManagementDesc": { + "message": "After making updates in the Bitwarden cloud server, upload your license file to apply the most recent changes." + }, + "addToFolder": { + "message": "Add to folder" + }, + "selectFolder": { + "message": "Select folder" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/fa/messages.json b/apps/web/src/locales/fa/messages.json index 033e4b6f7e5..50da37fea51 100644 --- a/apps/web/src/locales/fa/messages.json +++ b/apps/web/src/locales/fa/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "نشانی ایمیل" }, - "yourVaultIsLocked": { - "message": "گاوصندوق شما قفل است. برای ادامه کلمه عبور اصلی خود را وارد کنید." + "yourVaultIsLockedV2": { + "message": "Your vault is locked" }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "آیا اطمینان دارید که می‌خواهید ادامه دهید؟" }, "moveSelectedItemsDesc": { - "message": "پوشه ای را انتخاب کنید که می‌خواهید $COUNT$ مورد انتخاب شده را به آن منتقل کنید.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Scan the QR code below with your authenticator app or enter the key." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Could not load QR code. Try again or use the key below." + }, "key": { "message": "کلید" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "ایمیل حساب تأیید شد" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerifiedFailed": { "message": "تأیید ایمیل شما امکان پذیر نیست. سعی کنید یک ایمیل تأیید جدید ارسال کنید." }, @@ -7877,7 +7883,7 @@ "message": "Assign to these collections" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Select collections to assign" @@ -8439,6 +8445,9 @@ "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." }, + "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." + }, "higherKDFIterations": { "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." }, @@ -8524,5 +8533,47 @@ "noInvoicesToList": { "message": "There are no invoices to list", "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,", + "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": { + "message": "contact Bitwarden support.", + "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'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsored" + }, + "licenseAndBillingManagementDesc": { + "message": "After making updates in the Bitwarden cloud server, upload your license file to apply the most recent changes." + }, + "addToFolder": { + "message": "Add to folder" + }, + "selectFolder": { + "message": "Select folder" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/fi/messages.json b/apps/web/src/locales/fi/messages.json index 9e01af9828d..3516862c78f 100644 --- a/apps/web/src/locales/fi/messages.json +++ b/apps/web/src/locales/fi/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "Sähköpostiosoite" }, - "yourVaultIsLocked": { - "message": "Holvi on lukittu. Jatka vahvistamalla pääsalasanasi." + "yourVaultIsLockedV2": { + "message": "Your vault is locked" }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Haluatko varmasti jatkaa?" }, "moveSelectedItemsDesc": { - "message": "Valitse kansio, johon haluat siirtää $COUNT$ kohdetta.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Skannaa alla oleva QR-koodi todennussovelluksellasi tai syötä avain." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Could not load QR code. Try again or use the key below." + }, "key": { "message": "Avain" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "Tilin sähköpostiosoite on vahvistettu" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerifiedFailed": { "message": "Sähköpostiosoitettasi ei voitu vahvistaa. Yritä lähettää uusi vahvistussähköposti." }, @@ -3706,7 +3712,7 @@ } }, "subscriptionSeatMaxReached": { - "message": "You cannot invite more than $COUNT$ members without increasing your subscription seats.", + "message": "Voit kutsua enintään $COUNT$ jäsentä kasvattamatta tilauksesi käyttäjäpaikkojen määrää.", "placeholders": { "count": { "content": "$1", @@ -7877,7 +7883,7 @@ "message": "Määritä seuraaviin kokoelmiin" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Valitse kokoelmat, joihin kohteet sisällytetään. Kun kohdetta muokataan yhdessä kokoelmassa, päivittyy muutos kaikkiin kokoelmiin. Kohteet näkyvät vain niille organisaation jäsenille, joilla on näiden kokoelmien käyttöoikeus." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Valitse määritettävät kokoelmat" @@ -8439,6 +8445,9 @@ "memberAccessReportDesc": { "message": "Varmista, että jäsenillä on oikeiden käyttäjätietojen käyttöoikeus ja että heidän tilinsä on suojattu. Tämän raportin avulla saat CSV-tiedoston käyttäjien oikeuksista ja tiliasetuksista." }, + "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." + }, "higherKDFIterations": { "message": "Korkeampi KDF-toistojen määrä vahvistaa pääsalasanasi suojausta väsytyshyökkäyksien varalta." }, @@ -8522,7 +8531,49 @@ "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": { - "message": "Näytettäviä laskuja ei ole", + "message": "Listattavia laskuja ei ole", "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,", + "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": { + "message": "contact Bitwarden support.", + "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'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsored" + }, + "licenseAndBillingManagementDesc": { + "message": "After making updates in the Bitwarden cloud server, upload your license file to apply the most recent changes." + }, + "addToFolder": { + "message": "Add to folder" + }, + "selectFolder": { + "message": "Select folder" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/fil/messages.json b/apps/web/src/locales/fil/messages.json index f9c46bcc98f..6c72fd4d3ba 100644 --- a/apps/web/src/locales/fil/messages.json +++ b/apps/web/src/locales/fil/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "Email address" }, - "yourVaultIsLocked": { - "message": "Naka-lock ang vault mo. Beripikahin ang master password mo para tumuloy." + "yourVaultIsLockedV2": { + "message": "Your vault is locked" }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Sigurado ka bang gusto mong tumuloy?" }, "moveSelectedItemsDesc": { - "message": "Piliin ang folder na paglilipatan ng $COUNT$ napiling item.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Scan the QR code below with your authenticator app or enter the key." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Could not load QR code. Try again or use the key below." + }, "key": { "message": "Key" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "Na verify ang email ng account" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerifiedFailed": { "message": "Hindi ma verify ang iyong email. Subukan ang pagpapadala ng isang bagong email sa pag verify." }, @@ -7877,7 +7883,7 @@ "message": "Assign to these collections" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Select collections to assign" @@ -8439,6 +8445,9 @@ "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." }, + "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." + }, "higherKDFIterations": { "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." }, @@ -8524,5 +8533,47 @@ "noInvoicesToList": { "message": "There are no invoices to list", "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,", + "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": { + "message": "contact Bitwarden support.", + "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'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsored" + }, + "licenseAndBillingManagementDesc": { + "message": "After making updates in the Bitwarden cloud server, upload your license file to apply the most recent changes." + }, + "addToFolder": { + "message": "Add to folder" + }, + "selectFolder": { + "message": "Select folder" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/fr/messages.json b/apps/web/src/locales/fr/messages.json index 7e816e70f41..3f9e475ba75 100644 --- a/apps/web/src/locales/fr/messages.json +++ b/apps/web/src/locales/fr/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "Adresse électronique" }, - "yourVaultIsLocked": { - "message": "Votre coffre est verrouillé. Vérifiez votre mot de passe principal pour continuer." + "yourVaultIsLockedV2": { + "message": "Votre coffre est verrouillé." }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Êtes-vous sûr(e) de vouloir continuer ?" }, "moveSelectedItemsDesc": { - "message": "Choisissez le dossier vers lequel vous souhaitez déplacer les $COUNT$ élément(s) sélectionné(s).", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Scannez le code QR ci-dessous avec votre application d'authentification ou saisissez la clé." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Impossible de charger le code QR. Réessayez ou utilisez la clé ci-dessous." + }, "key": { "message": "Clé" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "Courriel du compte vérifié" }, + "emailVerifiedV2": { + "message": "Courriel vérifié" + }, "emailVerifiedFailed": { "message": "Impossible de vérifier votre courriel. Essayez en envoyant un nouveau courriel de vérification." }, @@ -7877,7 +7883,7 @@ "message": "Assigner à ces collections" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Sélectionnez les collections avec lesquelles les éléments seront partagés. Une fois qu'un élément est mis à jour dans une collection, il le sera aussi dans toutes ces collections. Seuls les membres de l'organisation ayant accès à ces collections pourront voir les éléments." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Sélectionnez les collections à assigner" @@ -8439,6 +8445,9 @@ "memberAccessReportDesc": { "message": "S'assurer que les membres ont accès aux bons identifiants et que leurs comptes sont sécurisés. Utilisez ce rapport pour obtenir un CSV d'accès aux membres et des configurations de compte." }, + "memberAccessReportPageDesc": { + "message": "Audite les accès des membres de l'organisation au sein des groupes, des collections et des éléments des collections. L'exportation CSV fournit un rapport détaillé par membre comprenant des informations sur les collections autorisées et les configurations de compte." + }, "higherKDFIterations": { "message": "Des itérations KDF plus élevées peuvent aider à protéger votre mot de passe principal contre la force brute d'un assaillant." }, @@ -8524,5 +8533,47 @@ "noInvoicesToList": { "message": "Il n'y a aucune facture à afficher", "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": "Remarque : Plus tard ce mois-ci, la confidentialité du coffre du client sera améliorée et les membres du fournisseur n'auront plus un accès direct aux éléments du coffre du client. Pour les questions,", + "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": { + "message": "contactez le support Bitwarden.", + "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'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Parrainé" + }, + "licenseAndBillingManagementDesc": { + "message": "Après avoir fait des mises à jour sur le serveur cloud de Bitwarden, téléversez votre fichier de licence pour appliquer les modifications les plus récentes." + }, + "addToFolder": { + "message": "Ajouter au dossier" + }, + "selectFolder": { + "message": "Sélectionner un dossier" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ seront transférés à l'organisation sélectionnée de façon permanente. Vous ne serez plus propriétaire de ces éléments.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ seront transférés à $ORG$ de façon permanente. Vous ne serez plus propriétaire de ces éléments.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/gl/messages.json b/apps/web/src/locales/gl/messages.json index bec3ddfbae9..2d8503f35b8 100644 --- a/apps/web/src/locales/gl/messages.json +++ b/apps/web/src/locales/gl/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "Email address" }, - "yourVaultIsLocked": { - "message": "Your vault is locked. Verify your master password to continue." + "yourVaultIsLockedV2": { + "message": "Your vault is locked" }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Are you sure you want to continue?" }, "moveSelectedItemsDesc": { - "message": "Choose a folder that you would like to move the $COUNT$ selected item(s) to.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Scan the QR code below with your authenticator app or enter the key." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Could not load QR code. Try again or use the key below." + }, "key": { "message": "Key" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "Account email verified" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerifiedFailed": { "message": "Unable to verify your email. Try sending a new verification email." }, @@ -7877,7 +7883,7 @@ "message": "Assign to these collections" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Select collections to assign" @@ -8439,6 +8445,9 @@ "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." }, + "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." + }, "higherKDFIterations": { "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." }, @@ -8524,5 +8533,47 @@ "noInvoicesToList": { "message": "There are no invoices to list", "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,", + "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": { + "message": "contact Bitwarden support.", + "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'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsored" + }, + "licenseAndBillingManagementDesc": { + "message": "After making updates in the Bitwarden cloud server, upload your license file to apply the most recent changes." + }, + "addToFolder": { + "message": "Add to folder" + }, + "selectFolder": { + "message": "Select folder" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/he/messages.json b/apps/web/src/locales/he/messages.json index afaad1a2366..0bfc56c60c3 100644 --- a/apps/web/src/locales/he/messages.json +++ b/apps/web/src/locales/he/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "כתובת אימייל" }, - "yourVaultIsLocked": { - "message": "הכספת שלך נעולה. הזן את הסיסמה הראשית שלך כדי להמשיך." + "yourVaultIsLockedV2": { + "message": "Your vault is locked" }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Are you sure you want to continue?" }, "moveSelectedItemsDesc": { - "message": "בחר תיקיה שאליה תרצה להעביר את $COUNT$ הפריט(ים) שבחרת.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Scan the QR code below with your authenticator app or enter the key." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Could not load QR code. Try again or use the key below." + }, "key": { "message": "מפתח" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "כתובת האימייל שלך אומתה." }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerifiedFailed": { "message": "לא ניתן לאמת את האימייל שלך. נסה לשלוח מייל אימות חדש." }, @@ -7877,7 +7883,7 @@ "message": "Assign to these collections" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Select collections to assign" @@ -8439,6 +8445,9 @@ "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." }, + "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." + }, "higherKDFIterations": { "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." }, @@ -8524,5 +8533,47 @@ "noInvoicesToList": { "message": "There are no invoices to list", "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,", + "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": { + "message": "contact Bitwarden support.", + "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'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsored" + }, + "licenseAndBillingManagementDesc": { + "message": "After making updates in the Bitwarden cloud server, upload your license file to apply the most recent changes." + }, + "addToFolder": { + "message": "Add to folder" + }, + "selectFolder": { + "message": "Select folder" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/hi/messages.json b/apps/web/src/locales/hi/messages.json index e48b9184a40..2a9f25a0e1f 100644 --- a/apps/web/src/locales/hi/messages.json +++ b/apps/web/src/locales/hi/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "Email address" }, - "yourVaultIsLocked": { - "message": "Your vault is locked. Verify your master password to continue." + "yourVaultIsLockedV2": { + "message": "Your vault is locked" }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Are you sure you want to continue?" }, "moveSelectedItemsDesc": { - "message": "Choose a folder that you would like to move the $COUNT$ selected item(s) to.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Scan the QR code below with your authenticator app or enter the key." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Could not load QR code. Try again or use the key below." + }, "key": { "message": "Key" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "Account email verified" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerifiedFailed": { "message": "Unable to verify your email. Try sending a new verification email." }, @@ -7877,7 +7883,7 @@ "message": "Assign to these collections" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Select collections to assign" @@ -8439,6 +8445,9 @@ "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." }, + "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." + }, "higherKDFIterations": { "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." }, @@ -8524,5 +8533,47 @@ "noInvoicesToList": { "message": "There are no invoices to list", "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,", + "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": { + "message": "contact Bitwarden support.", + "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'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsored" + }, + "licenseAndBillingManagementDesc": { + "message": "After making updates in the Bitwarden cloud server, upload your license file to apply the most recent changes." + }, + "addToFolder": { + "message": "Add to folder" + }, + "selectFolder": { + "message": "Select folder" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/hr/messages.json b/apps/web/src/locales/hr/messages.json index 14b2a995405..c6901bd70c7 100644 --- a/apps/web/src/locales/hr/messages.json +++ b/apps/web/src/locales/hr/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "Adresa e-pošte" }, - "yourVaultIsLocked": { - "message": "Tvoj trezor je zaključan. Potvrdi glavnu lozinku za nastavak." + "yourVaultIsLockedV2": { + "message": "Your vault is locked" }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Sigurno želiš nastaviti?" }, "moveSelectedItemsDesc": { - "message": "Odaberi mapu u koju želiš premjestiti odabranih $COUNT$ stavke/i.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Scan the QR code below with your authenticator app or enter the key." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Could not load QR code. Try again or use the key below." + }, "key": { "message": "Ključ" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "Adresa e-pošte je provjerena" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerifiedFailed": { "message": "Ne možeš potvrditi svoju e-poštu? Pošalji novu poruku." }, @@ -7877,7 +7883,7 @@ "message": "Assign to these collections" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Select collections to assign" @@ -8439,6 +8445,9 @@ "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." }, + "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." + }, "higherKDFIterations": { "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." }, @@ -8524,5 +8533,47 @@ "noInvoicesToList": { "message": "There are no invoices to list", "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,", + "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": { + "message": "contact Bitwarden support.", + "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'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsored" + }, + "licenseAndBillingManagementDesc": { + "message": "After making updates in the Bitwarden cloud server, upload your license file to apply the most recent changes." + }, + "addToFolder": { + "message": "Add to folder" + }, + "selectFolder": { + "message": "Select folder" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/hu/messages.json b/apps/web/src/locales/hu/messages.json index 2fe73008f6e..dacc855d7b1 100644 --- a/apps/web/src/locales/hu/messages.json +++ b/apps/web/src/locales/hu/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "Email cím" }, - "yourVaultIsLocked": { - "message": "A széf zárolásra került. A folytatáshoz meg kell adni a mesterjelszót." + "yourVaultIsLockedV2": { + "message": "A széf zárolva van." }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Biztos folytatni szeretnénk?" }, "moveSelectedItemsDesc": { - "message": "Célmappa kiválasztás $COUNT$ kijelölt elem áthelyezéséhez.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Olvassuk be az alábbi QR kódot a hitelesítő alkalmazással,vagy írjuk be a kulcsot." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Nem sikerült betölteni a QR-kódot. Próbáljuk újra vagy használjuk az alábbi kulcsot." + }, "key": { "message": "Kulcs" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "Az email cím megerősítésre került." }, + "emailVerifiedV2": { + "message": "Az email cím ellenőrzésre került." + }, "emailVerifiedFailed": { "message": "Nem sikerült az email cím ellenőrzése. Új ellenőrző email küldése." }, @@ -7877,7 +7883,7 @@ "message": "Hozzárendelés ezen gyűjteményekhez" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Válasszuk ki azokat a gyűjteményeket, amelyekkel az elemek megosztásra kerülnek. Ha egy elem egy gyűjteményben frissítésre kerül, az az összes gyűjteményben megjelenik. Csak az ezekhez a gyűjteményekhez hozzáféréssel rendelkező szervezeti tagok láthatják az elemeket." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Hozzárendelendő gyűjtemények kiválasztása" @@ -8439,6 +8445,9 @@ "memberAccessReportDesc": { "message": "Győződjünk meg arról, hogy a tagok hozzáférnek a megfelelő hitelesítő adatokhoz és fiókjaik biztonságosak. Ezzel a jelentéssel szerezhetjük be a tagok hozzáférését és a fiókkonfigurációkat tartalmazó CSV fájlt." }, + "memberAccessReportPageDesc": { + "message": "A szervezeti tagok hozzáférésének ellenőrzése a csoportok, gyűjtemények és gyűjteményelemek között. A CSV exportálás tagonként részletes lebontást biztosít, beleértve a gyűjtemény engedélyekre és a fiókkonfigurációkra vonatkozó információkat." + }, "higherKDFIterations": { "message": "A magasabb szintű KDF iterációk segíthetnek megvédeni mesterjelszót a támadók erőszakossága ellen." }, @@ -8524,5 +8533,47 @@ "noInvoicesToList": { "message": "Nincsenek listázandó számlák.", "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,", + "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": { + "message": "contact Bitwarden support.", + "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'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Szponzorált" + }, + "licenseAndBillingManagementDesc": { + "message": "A Bitwarden felhőkiszolgáló frissítése után töltsük fel a licenszfájlt a legutóbbi módosítások alkalmazásához." + }, + "addToFolder": { + "message": "Add to folder" + }, + "selectFolder": { + "message": "Select folder" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/id/messages.json b/apps/web/src/locales/id/messages.json index 2a6cbe38099..a12e45cddfd 100644 --- a/apps/web/src/locales/id/messages.json +++ b/apps/web/src/locales/id/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "Alamat Surel" }, - "yourVaultIsLocked": { - "message": "Brankas Anda terkunci. Verifikasi kata sandi utama Anda untuk melanjutkan." + "yourVaultIsLockedV2": { + "message": "Your vault is locked" }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Are you sure you want to continue?" }, "moveSelectedItemsDesc": { - "message": "Pilih folder tempat Anda ingin memindahkan $COUNT$ item yang dipilih.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Scan the QR code below with your authenticator app or enter the key." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Could not load QR code. Try again or use the key below." + }, "key": { "message": "Kunci" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "Email Anda telah diverifikasi." }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerifiedFailed": { "message": "Tidak dapat memverifikasi email Anda. Coba kirim email verifikasi baru." }, @@ -7877,7 +7883,7 @@ "message": "Assign to these collections" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Select collections to assign" @@ -8439,6 +8445,9 @@ "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." }, + "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." + }, "higherKDFIterations": { "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." }, @@ -8524,5 +8533,47 @@ "noInvoicesToList": { "message": "There are no invoices to list", "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,", + "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": { + "message": "contact Bitwarden support.", + "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'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsored" + }, + "licenseAndBillingManagementDesc": { + "message": "After making updates in the Bitwarden cloud server, upload your license file to apply the most recent changes." + }, + "addToFolder": { + "message": "Add to folder" + }, + "selectFolder": { + "message": "Select folder" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/it/messages.json b/apps/web/src/locales/it/messages.json index dde0df5a99f..59332396b64 100644 --- a/apps/web/src/locales/it/messages.json +++ b/apps/web/src/locales/it/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "Indirizzo email" }, - "yourVaultIsLocked": { - "message": "La tua cassaforte è bloccata. Verifica la tua password principale per continuare." + "yourVaultIsLockedV2": { + "message": "Your vault is locked" }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Sei sicuro di voler continuare?" }, "moveSelectedItemsDesc": { - "message": "Scegli una cartella in cui vuoi spostare i $COUNT$ elementi selezionati.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Scan the QR code below with your authenticator app or enter the key." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Could not load QR code. Try again or use the key below." + }, "key": { "message": "Chiave" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "Email account verificata" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerifiedFailed": { "message": "Impossibile verificare la tua email. Prova a inviare una nuova email di verifica." }, @@ -7877,7 +7883,7 @@ "message": "Assegna a queste raccolte" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Seleziona le raccolte con cui questi elementi saranno condivisi. Una volta un elemento è aggiornato in una raccolta, la modifica si rifletterà in tutte le raccolte. Solo i membri dell'organizzazione con accesso a queste raccolte potranno visualizzare gli elementi." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Seleziona le raccolte da assegnare" @@ -8439,6 +8445,9 @@ "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." }, + "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." + }, "higherKDFIterations": { "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." }, @@ -8524,5 +8533,47 @@ "noInvoicesToList": { "message": "There are no invoices to list", "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,", + "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": { + "message": "contact Bitwarden support.", + "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'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsored" + }, + "licenseAndBillingManagementDesc": { + "message": "After making updates in the Bitwarden cloud server, upload your license file to apply the most recent changes." + }, + "addToFolder": { + "message": "Add to folder" + }, + "selectFolder": { + "message": "Select folder" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/ja/messages.json b/apps/web/src/locales/ja/messages.json index 5480984ab78..53db2390d1c 100644 --- a/apps/web/src/locales/ja/messages.json +++ b/apps/web/src/locales/ja/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "メールアドレス" }, - "yourVaultIsLocked": { - "message": "保管庫がロックされています。開くにはマスターパスワードを入力してください。" + "yourVaultIsLockedV2": { + "message": "保管庫はロックされています。" }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "本当に続行しますか?" }, "moveSelectedItemsDesc": { - "message": "$COUNT$個のアイテムを移動したいフォルダーを選択してください。", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "以下の QR コードを認証アプリでスキャンするか、キーを入力してください。" }, + "twoStepAuthenticatorQRCanvasError": { + "message": "QR コードを読み込めませんでした。もう一度試すか、以下のキーを使用してください。" + }, "key": { "message": "キー" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "メールアドレスが確認されました。" }, + "emailVerifiedV2": { + "message": "メールアドレスを認証しました" + }, "emailVerifiedFailed": { "message": "メールアドレスを確認できませんでした。確認メールを再送信してください。" }, @@ -7877,7 +7883,7 @@ "message": "これらのコレクションに割り当てる" }, "bulkCollectionAssignmentDialogDescription": { - "message": "アイテムを共有するコレクションを選択します。1つのコレクションでアイテムが更新されると、すべてのコレクションに反映されます。これらのコレクションにアクセスできる組織メンバーだけがアイテムを見ることができます。" + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "割り当てるコレクションを選択" @@ -8439,6 +8445,9 @@ "memberAccessReportDesc": { "message": "メンバーが適切な資格情報にアクセスでき、アカウントが安全であることを確認します。 このレポートを使用するとメンバーアクセスとアカウント設定の CSV を取得できます。" }, + "memberAccessReportPageDesc": { + "message": "組織メンバーによるグループ、コレクション、コレクションアイテム間のアクセスを監査します。 CSV エクスポートには、コレクションの権限とアカウント構成に関する情報を含むメンバーごとの詳細な内訳が表示されます。" + }, "higherKDFIterations": { "message": "KDF 反復回数を多くすることで、攻撃者による総当たり攻撃からマスターパスワードを守ることができます。" }, @@ -8524,5 +8533,47 @@ "noInvoicesToList": { "message": "一覧表示する請求書がありません", "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": "お知らせ: 今月後半にクライアント保管庫のプライバシーが改善され、プロバイダのメンバーはクライアント保管庫のアイテムに直接アクセスできなくなります。", + "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": { + "message": "詳細は Bitwarden サポートにお問い合わせください。", + "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'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsored" + }, + "licenseAndBillingManagementDesc": { + "message": "Bitwarden クラウドサーバーで更新を行った後、ライセンスファイルをアップロードして最新の変更を適用してください。" + }, + "addToFolder": { + "message": "フォルダーに追加" + }, + "selectFolder": { + "message": "フォルダーを選択" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ は選択した組織に恒久的に移行されます。これらのアイテムはあなたの所有ではなくなります。", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ は $ORG$ に恒久的に移行されます。これらのアイテムはあなたの所有ではなくなります。", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/ka/messages.json b/apps/web/src/locales/ka/messages.json index f6df06494b4..1a160bd22ac 100644 --- a/apps/web/src/locales/ka/messages.json +++ b/apps/web/src/locales/ka/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "ელ-ფოსტის მისამართი" }, - "yourVaultIsLocked": { - "message": "თქვენი საცავი ჩაკეტილია. დაადასტურეთ თქვენი მთავარი პაროლი გასაგრძელებლად." + "yourVaultIsLockedV2": { + "message": "Your vault is locked" }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "დარწმუნებული ხართ რომ გინდათ გაგრძელება?" }, "moveSelectedItemsDesc": { - "message": "აირჩიეთ საქაღალდე რომელშიც გსურთ გადაიტანოთ $COUNT$ არჩეული საგნი(ები).", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Scan the QR code below with your authenticator app or enter the key." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Could not load QR code. Try again or use the key below." + }, "key": { "message": "Key" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "Account email verified" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerifiedFailed": { "message": "Unable to verify your email. Try sending a new verification email." }, @@ -7877,7 +7883,7 @@ "message": "Assign to these collections" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Select collections to assign" @@ -8439,6 +8445,9 @@ "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." }, + "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." + }, "higherKDFIterations": { "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." }, @@ -8524,5 +8533,47 @@ "noInvoicesToList": { "message": "There are no invoices to list", "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,", + "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": { + "message": "contact Bitwarden support.", + "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'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsored" + }, + "licenseAndBillingManagementDesc": { + "message": "After making updates in the Bitwarden cloud server, upload your license file to apply the most recent changes." + }, + "addToFolder": { + "message": "Add to folder" + }, + "selectFolder": { + "message": "Select folder" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/km/messages.json b/apps/web/src/locales/km/messages.json index bec3ddfbae9..2d8503f35b8 100644 --- a/apps/web/src/locales/km/messages.json +++ b/apps/web/src/locales/km/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "Email address" }, - "yourVaultIsLocked": { - "message": "Your vault is locked. Verify your master password to continue." + "yourVaultIsLockedV2": { + "message": "Your vault is locked" }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Are you sure you want to continue?" }, "moveSelectedItemsDesc": { - "message": "Choose a folder that you would like to move the $COUNT$ selected item(s) to.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Scan the QR code below with your authenticator app or enter the key." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Could not load QR code. Try again or use the key below." + }, "key": { "message": "Key" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "Account email verified" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerifiedFailed": { "message": "Unable to verify your email. Try sending a new verification email." }, @@ -7877,7 +7883,7 @@ "message": "Assign to these collections" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Select collections to assign" @@ -8439,6 +8445,9 @@ "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." }, + "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." + }, "higherKDFIterations": { "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." }, @@ -8524,5 +8533,47 @@ "noInvoicesToList": { "message": "There are no invoices to list", "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,", + "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": { + "message": "contact Bitwarden support.", + "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'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsored" + }, + "licenseAndBillingManagementDesc": { + "message": "After making updates in the Bitwarden cloud server, upload your license file to apply the most recent changes." + }, + "addToFolder": { + "message": "Add to folder" + }, + "selectFolder": { + "message": "Select folder" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/kn/messages.json b/apps/web/src/locales/kn/messages.json index 999702fdaf5..4cbbd331350 100644 --- a/apps/web/src/locales/kn/messages.json +++ b/apps/web/src/locales/kn/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "ಇಮೇಲ್ ವಿಳಾಸ" }, - "yourVaultIsLocked": { - "message": "ನಿಮ್ಮ ವಾಲ್ಟ್ ಲಾಕ್ ಆಗಿದೆ. ಮುಂದುವರೆಯಲು ನಿಮ್ಮ ಮಾಸ್ಟರ್ ಪಾಸ್‌ವರ್ಡ್ ಅನ್ನು ಪರಿಶೀಲಿಸಿ." + "yourVaultIsLockedV2": { + "message": "Your vault is locked" }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Are you sure you want to continue?" }, "moveSelectedItemsDesc": { - "message": "ನೀವು $COUNT$ ಆಯ್ದ ಐಟಂ (ಗಳನ್ನು) ಗೆ ಸರಿಸಲು ಬಯಸುವ ಫೋಲ್ಡರ್ ಆಯ್ಕೆಮಾಡಿ.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Scan the QR code below with your authenticator app or enter the key." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Could not load QR code. Try again or use the key below." + }, "key": { "message": "ಕೀ" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "ನಿಮ್ಮ ಇಮೇಲ್ ಅನ್ನು ಪರಿಶೀಲಿಸಲಾಗಿದೆ." }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerifiedFailed": { "message": "ನಿಮ್ಮ ಇಮೇಲ್ ಅನ್ನು ಪರಿಶೀಲಿಸಲು ಸಾಧ್ಯವಿಲ್ಲ. ಹೊಸ ಪರಿಶೀಲನೆ ಇಮೇಲ್ ಕಳುಹಿಸಲು ಪ್ರಯತ್ನಿಸಿ." }, @@ -7877,7 +7883,7 @@ "message": "Assign to these collections" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Select collections to assign" @@ -8439,6 +8445,9 @@ "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." }, + "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." + }, "higherKDFIterations": { "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." }, @@ -8524,5 +8533,47 @@ "noInvoicesToList": { "message": "There are no invoices to list", "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,", + "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": { + "message": "contact Bitwarden support.", + "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'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsored" + }, + "licenseAndBillingManagementDesc": { + "message": "After making updates in the Bitwarden cloud server, upload your license file to apply the most recent changes." + }, + "addToFolder": { + "message": "Add to folder" + }, + "selectFolder": { + "message": "Select folder" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/ko/messages.json b/apps/web/src/locales/ko/messages.json index 2e8c9d9e6c3..98df1b9e04a 100644 --- a/apps/web/src/locales/ko/messages.json +++ b/apps/web/src/locales/ko/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "이메일 주소" }, - "yourVaultIsLocked": { - "message": "보관함이 잠겨 있습니다. 마스터 비밀번호를 입력하여 계속하세요." + "yourVaultIsLockedV2": { + "message": "Your vault is locked" }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "정말 계속하시겠습니까?" }, "moveSelectedItemsDesc": { - "message": "선택된 $COUNT$ 개의 항목을 옮길 폴더를 선택하십시오.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Scan the QR code below with your authenticator app or enter the key." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Could not load QR code. Try again or use the key below." + }, "key": { "message": "키" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "이메일이 확인되었습니다." }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerifiedFailed": { "message": "이메일을 인증할 수 없습니다. 새로운 인증을 이메일로 전송하십시오." }, @@ -7877,7 +7883,7 @@ "message": "Assign to these collections" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Select collections to assign" @@ -8439,6 +8445,9 @@ "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." }, + "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." + }, "higherKDFIterations": { "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." }, @@ -8524,5 +8533,47 @@ "noInvoicesToList": { "message": "There are no invoices to list", "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,", + "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": { + "message": "contact Bitwarden support.", + "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'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsored" + }, + "licenseAndBillingManagementDesc": { + "message": "After making updates in the Bitwarden cloud server, upload your license file to apply the most recent changes." + }, + "addToFolder": { + "message": "Add to folder" + }, + "selectFolder": { + "message": "Select folder" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/lv/messages.json b/apps/web/src/locales/lv/messages.json index 21169c61beb..c8e23e455c0 100644 --- a/apps/web/src/locales/lv/messages.json +++ b/apps/web/src/locales/lv/messages.json @@ -174,7 +174,7 @@ "description": "This is the folder for uncategorized items" }, "selfOwnershipLabel": { - "message": "You", + "message": "Tu", "description": "Used as a label to indicate that the user is the owner of an item." }, "addFolder": { @@ -406,13 +406,13 @@ "message": "Vienums" }, "itemDetails": { - "message": "Item details" + "message": "Vienuma dati" }, "itemName": { - "message": "Item name" + "message": "Vienuma nosaukums" }, "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "message": "Nevar noņemt krājumus ar tiesībām \"Tikai skatīt\": $COLLECTIONS$", "placeholders": { "collections": { "content": "$1", @@ -854,8 +854,8 @@ "emailAddress": { "message": "E-pasta adrese" }, - "yourVaultIsLocked": { - "message": "Glabātava ir aizslēgta. Nepieciešams norādīt galveno paroli, lai turpinātu." + "yourVaultIsLockedV2": { + "message": "Glabātava ir slēgta." }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Vai tiešām vēlaties turpināt?" }, "moveSelectedItemsDesc": { - "message": "Izvēlēties mapi, uz kuru pārvietot atlasīto(s) $COUNT$ vienumu(s).", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Zemāk esošais kvadrātkods jānolasa ar autnetificētāja lietotni vai jāievada atslēga." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Nevarēja ielādēt kvadrātkodu. Jāmēģina vēlreiz vai jāizmanto zemāk esošā atslēga." + }, "key": { "message": "Atslēga" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "E-pasta adrese ir apstiprināta." }, + "emailVerifiedV2": { + "message": "E-pasta adrese ir apliecināta" + }, "emailVerifiedFailed": { "message": "Nevar apstiprināt e-pasta adresi. Var mēģināt sūtīt atkārtotu apstiprinājuma e-pasta ziņojumu." }, @@ -3706,7 +3712,7 @@ } }, "subscriptionSeatMaxReached": { - "message": "You cannot invite more than $COUNT$ members without increasing your subscription seats.", + "message": "Nevar uzaicināt vairāk kā $COUNT$ dalībnieku(s) bez abonementa vietu skaita palielināšanas.", "placeholders": { "count": { "content": "$1", @@ -6804,7 +6810,7 @@ "message": "Automātiska domēnu apstiprināšana" }, "automaticDomainVerificationProcess": { - "message": "Bitwarden mēģinās pārbaudīt domēnu 3 reizes pirmajās 72 stundās. Ja domēnu nevarēs apstiprināt, būs jāpārbauda DNS ieraksts saimniekdatorā un pašrocīgi jāapstiprina. Domēns tiks noņemts no apvienības pēc 7 dienām, ja tas nebūs apstiprināts" + "message": "Bitwarden mēģinās pārbaudīt domēnu 3 reizes pirmajās 72 stundās. Ja domēnu nevarēs apliecināt, būs jāpārbauda DNS ieraksts saimniekdatorā un pašrocīgi tas jāapliecina. Domēns tiks noņemts no apvienības pēc 7 dienām, ja tas nebūs apliecināts" }, "invalidDomainNameMessage": { "message": "Ievadītā vērtība ir nederīga. Piemēram: mansdomens.lv. Apakšdomēniem ir nepieciešams apstiprināt atsevišķus ierakstus." @@ -6822,7 +6828,7 @@ "message": "Domēns ir saglabāts" }, "domainVerified": { - "message": "Domēns ir apstiprināts" + "message": "Domēns ir apliecināts" }, "duplicateDomainError": { "message": "Vienu domēnu nevar pieprasīt divreiz." @@ -7877,7 +7883,7 @@ "message": "Piešķirt šiem krājumiem" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Jāatlasa krājumi, ar kuriem vienumi tiks kopīgoti. Tiklīdz kāds vienums tiks atjaunināts vienā krājumā, tas atspoguļosies visos pārējos. Tikai apvienības dalībnieki ar piekļuvi šiem krājumiem varēs redzēt vienumus." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Atlasīt krājumus, lai piešķirtu" @@ -8439,6 +8445,9 @@ "memberAccessReportDesc": { "message": "Nodrošini, ka dalībniekiem ir piekļuve pareizajiem piekļuves datiem un viņu konti ir droši. Šī atskaite ir izmantojama, lai iegūtu CSV ar dalībnieku piekļuvi un kontu konfigurāciju." }, + "memberAccessReportPageDesc": { + "message": "Pārskatīt apvienības dalībnieku piekļuvi dažādām kopām, krājumiem un krājumu vienumiem. CSV izgūšana sniedz izvērstu pārskatu par katru dalībnieku, tajā skaitā informāciju par krājumu atļaujām un konta konfigurāciju." + }, "higherKDFIterations": { "message": "Lielāks KDF atkārtojumu skaits var palīdzēt aizsargāt galveno paroli pārlases uzbrukuma gadījumā." }, @@ -8512,17 +8521,59 @@ "message": "Lejupielādēt CSV" }, "monthlySubscriptionUserSeatsMessage": { - "message": "Adjustments to your subscription will result in prorated charges to your billing totals on your next billing period. " + "message": "Abonementa pielāgojumi izvērtīsies attiecīgās izmaksās kopējā rēķinā nākamajā norēķinu laika posmā. " }, "annualSubscriptionUserSeatsMessage": { - "message": "Adjustments to your subscription will result in prorated charges on a monthly billing cycle. " + "message": "Abonementa pielāgojumi izvērtīsies attiecīgās izmaksās ikmēneša norēķinos. " }, "billingHistoryDescription": { "message": "Lejupielādēt CSV, lai iegūtu informāciju par klientiem katrā norēķinu datumā. Samērīgā sadalījuma maksas netiek iekļautas CSV un var atšķirties no saistītā rēķina. Visatbilstošāko norēķinu informāciju var iegūt ikmēneša rēķinos.", "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": { - "message": "There are no invoices to list", + "message": "Nav rēķinu, ko parādīt", "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": "Paziņojums: šajā mēnesī klienta glabātavas privātums tiks uzlabots, un nodrošinātāja dalībniekiem vairs nebūs tiešas piekļuves klienta glabātavas vienumiem. Ar jautājumiem", + "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": { + "message": "vērsties pie Bitwarden atbalsta.", + "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'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsorēts" + }, + "licenseAndBillingManagementDesc": { + "message": "Pēc atjauninājumu veikšanas Bitwarden mākoņa serverī jāaugšupielādē sava licences datne, lai pielietotu nesenākās izmaiņas." + }, + "addToFolder": { + "message": "Pievienot mapei" + }, + "selectFolder": { + "message": "Atlasīt mapi" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ tiks neatgriezeniski nodoti atlasītajai apvienībai. Šie vienumi Tev vairs nepiederēs.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ tiks neatgriezeniski nodoti $ORG$. Šie vienumi Tev vairs nepiederēs.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/ml/messages.json b/apps/web/src/locales/ml/messages.json index b9925a52288..48b91ad1db6 100644 --- a/apps/web/src/locales/ml/messages.json +++ b/apps/web/src/locales/ml/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "ഇ-മെയിൽ വിലാസം" }, - "yourVaultIsLocked": { - "message": "നിങ്ങളുടെ വാൾട് പൂട്ടിയിരിക്കുന്നു. തുടരുന്നതിന് നിങ്ങളുടെ പ്രാഥമിക പാസ്‌വേഡ് സ്ഥിരീകരിക്കുക." + "yourVaultIsLockedV2": { + "message": "Your vault is locked" }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Are you sure you want to continue?" }, "moveSelectedItemsDesc": { - "message": "$COUNT$ തിരഞ്ഞെടുത്ത ഇനങ്ങൾ നീക്കാൻ ആഗ്രഹിക്കുന്ന ഒരു ഫോൾഡർ തിരഞ്ഞെടുക്കുക).", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Scan the QR code below with your authenticator app or enter the key." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Could not load QR code. Try again or use the key below." + }, "key": { "message": "Key" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "നിങ്ങളുടെ ഇമെയിൽ സ്ഥിരീകരിച്ചു." }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerifiedFailed": { "message": "നിങ്ങളുടെ ഇമെയിൽ പരിശോധിച്ചുറപ്പിക്കാനായില്ല. ഒരു പുതിയ സ്ഥിരീകരണ ഇമെയിൽ അയയ്‌ക്കാൻ ശ്രമിക്കുക." }, @@ -7877,7 +7883,7 @@ "message": "Assign to these collections" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Select collections to assign" @@ -8439,6 +8445,9 @@ "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." }, + "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." + }, "higherKDFIterations": { "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." }, @@ -8524,5 +8533,47 @@ "noInvoicesToList": { "message": "There are no invoices to list", "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,", + "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": { + "message": "contact Bitwarden support.", + "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'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsored" + }, + "licenseAndBillingManagementDesc": { + "message": "After making updates in the Bitwarden cloud server, upload your license file to apply the most recent changes." + }, + "addToFolder": { + "message": "Add to folder" + }, + "selectFolder": { + "message": "Select folder" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/mr/messages.json b/apps/web/src/locales/mr/messages.json index bec3ddfbae9..2d8503f35b8 100644 --- a/apps/web/src/locales/mr/messages.json +++ b/apps/web/src/locales/mr/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "Email address" }, - "yourVaultIsLocked": { - "message": "Your vault is locked. Verify your master password to continue." + "yourVaultIsLockedV2": { + "message": "Your vault is locked" }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Are you sure you want to continue?" }, "moveSelectedItemsDesc": { - "message": "Choose a folder that you would like to move the $COUNT$ selected item(s) to.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Scan the QR code below with your authenticator app or enter the key." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Could not load QR code. Try again or use the key below." + }, "key": { "message": "Key" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "Account email verified" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerifiedFailed": { "message": "Unable to verify your email. Try sending a new verification email." }, @@ -7877,7 +7883,7 @@ "message": "Assign to these collections" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Select collections to assign" @@ -8439,6 +8445,9 @@ "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." }, + "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." + }, "higherKDFIterations": { "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." }, @@ -8524,5 +8533,47 @@ "noInvoicesToList": { "message": "There are no invoices to list", "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,", + "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": { + "message": "contact Bitwarden support.", + "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'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsored" + }, + "licenseAndBillingManagementDesc": { + "message": "After making updates in the Bitwarden cloud server, upload your license file to apply the most recent changes." + }, + "addToFolder": { + "message": "Add to folder" + }, + "selectFolder": { + "message": "Select folder" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/my/messages.json b/apps/web/src/locales/my/messages.json index bec3ddfbae9..2d8503f35b8 100644 --- a/apps/web/src/locales/my/messages.json +++ b/apps/web/src/locales/my/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "Email address" }, - "yourVaultIsLocked": { - "message": "Your vault is locked. Verify your master password to continue." + "yourVaultIsLockedV2": { + "message": "Your vault is locked" }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Are you sure you want to continue?" }, "moveSelectedItemsDesc": { - "message": "Choose a folder that you would like to move the $COUNT$ selected item(s) to.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Scan the QR code below with your authenticator app or enter the key." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Could not load QR code. Try again or use the key below." + }, "key": { "message": "Key" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "Account email verified" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerifiedFailed": { "message": "Unable to verify your email. Try sending a new verification email." }, @@ -7877,7 +7883,7 @@ "message": "Assign to these collections" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Select collections to assign" @@ -8439,6 +8445,9 @@ "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." }, + "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." + }, "higherKDFIterations": { "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." }, @@ -8524,5 +8533,47 @@ "noInvoicesToList": { "message": "There are no invoices to list", "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,", + "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": { + "message": "contact Bitwarden support.", + "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'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsored" + }, + "licenseAndBillingManagementDesc": { + "message": "After making updates in the Bitwarden cloud server, upload your license file to apply the most recent changes." + }, + "addToFolder": { + "message": "Add to folder" + }, + "selectFolder": { + "message": "Select folder" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/nb/messages.json b/apps/web/src/locales/nb/messages.json index 80251aa3a81..b97916165e6 100644 --- a/apps/web/src/locales/nb/messages.json +++ b/apps/web/src/locales/nb/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "E-postadresse" }, - "yourVaultIsLocked": { - "message": "Hvelvet ditt er låst. Kontroller hovedpassordet ditt for å fortsette." + "yourVaultIsLockedV2": { + "message": "Your vault is locked" }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Er du sikker på at du vil fortsette?" }, "moveSelectedItemsDesc": { - "message": "Velg en mappe som du ønsker å flytte $COUNT$ valgt(e) gjenstand(er) til.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Scan the QR code below with your authenticator app or enter the key." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Could not load QR code. Try again or use the key below." + }, "key": { "message": "Nøkkel" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "Din E-postadresse har blitt bekreftet." }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerifiedFailed": { "message": "Klarte ikke å bekrefte E-postadressen din. Prøv å sende en ny bekreftelses-E-post." }, @@ -7877,7 +7883,7 @@ "message": "Assign to these collections" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Select collections to assign" @@ -8439,6 +8445,9 @@ "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." }, + "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." + }, "higherKDFIterations": { "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." }, @@ -8524,5 +8533,47 @@ "noInvoicesToList": { "message": "There are no invoices to list", "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,", + "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": { + "message": "contact Bitwarden support.", + "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'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsored" + }, + "licenseAndBillingManagementDesc": { + "message": "After making updates in the Bitwarden cloud server, upload your license file to apply the most recent changes." + }, + "addToFolder": { + "message": "Add to folder" + }, + "selectFolder": { + "message": "Select folder" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/ne/messages.json b/apps/web/src/locales/ne/messages.json index e7ebce67fc8..7a9f598015c 100644 --- a/apps/web/src/locales/ne/messages.json +++ b/apps/web/src/locales/ne/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "Email address" }, - "yourVaultIsLocked": { - "message": "Your vault is locked. Verify your master password to continue." + "yourVaultIsLockedV2": { + "message": "Your vault is locked" }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Are you sure you want to continue?" }, "moveSelectedItemsDesc": { - "message": "Choose a folder that you would like to move the $COUNT$ selected item(s) to.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Scan the QR code below with your authenticator app or enter the key." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Could not load QR code. Try again or use the key below." + }, "key": { "message": "Key" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "Account email verified" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerifiedFailed": { "message": "Unable to verify your email. Try sending a new verification email." }, @@ -7877,7 +7883,7 @@ "message": "Assign to these collections" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Select collections to assign" @@ -8439,6 +8445,9 @@ "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." }, + "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." + }, "higherKDFIterations": { "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." }, @@ -8524,5 +8533,47 @@ "noInvoicesToList": { "message": "There are no invoices to list", "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,", + "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": { + "message": "contact Bitwarden support.", + "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'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsored" + }, + "licenseAndBillingManagementDesc": { + "message": "After making updates in the Bitwarden cloud server, upload your license file to apply the most recent changes." + }, + "addToFolder": { + "message": "Add to folder" + }, + "selectFolder": { + "message": "Select folder" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/nl/messages.json b/apps/web/src/locales/nl/messages.json index 72b4653c1bd..d804d74df65 100644 --- a/apps/web/src/locales/nl/messages.json +++ b/apps/web/src/locales/nl/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "E-mailadres" }, - "yourVaultIsLocked": { - "message": "Je kluis is vergrendeld. Voer je hoofdwachtwoord in om door te gaan." + "yourVaultIsLockedV2": { + "message": "Je kluis is vergrendeld." }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Weet je zeker dat je wilt doorgaan?" }, "moveSelectedItemsDesc": { - "message": "Kies een map waar je de $COUNT$ geselecteerde item(s) heen wilt verplaatsen.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Scan de onderstaande QR-code met je authenticator-app of voer de sleutel in." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Kan de QR-code niet laden. Probeer het opnieuw of gebruik de onderstaande sleutel." + }, "key": { "message": "Sleutel" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "Je e-mailadres is geverifieerd." }, + "emailVerifiedV2": { + "message": "E-mailadres geverifieerd" + }, "emailVerifiedFailed": { "message": "Je e-mailadres kon niet worden geverifieerd. Probeer een nieuwe e-mail met verificatielink te versturen." }, @@ -7877,7 +7883,7 @@ "message": "Aan deze collecties toewijzen" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Selecteer de collecies om de items mee te delen. Zodra een item in een collectie is bijgewerkt, werkt dat door in alle collecties. Alleen organisatieleden met toegang tot deze collecties kunnen de items zien." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Collecties voor toewijzen selecteren" @@ -8439,6 +8445,9 @@ "memberAccessReportDesc": { "message": "Zorg ervoor dat leden toegang hebben tot de juiste inloggegevens en dat hun accounts veilig zijn. Dit CSV-rapport geeft inzicht in ledentoegang en accountconfiguraties." }, + "memberAccessReportPageDesc": { + "message": "Audit de toegang van een organisatielid tot groepen, verzamelingen en verzamelen van items. De CSV-export biedt een gedetailleerde verdeling per lid, inclusief informatie over verzamelrechten en accountconfiguraties." + }, "higherKDFIterations": { "message": "Hogere KDF-iteraties beschermen je hoofdwachtwoord tegen brute-foce-aanvallen." }, @@ -8524,5 +8533,47 @@ "noInvoicesToList": { "message": "Er zijn geen facturen om weer te geven", "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": "Let op: Later deze maand wordt de privacy van kluis verbeterd en hebben leden geen directe toegang meer tot kluisitems van de client. Voor vragen,", + "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": { + "message": "contact op Bitwarden support.", + "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'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Gesponsord" + }, + "licenseAndBillingManagementDesc": { + "message": "Na het bijwerken in de Bitwarden-cloud-server, upload je je licentiebestand voor het toepassen van de meest recente wijzigingen." + }, + "addToFolder": { + "message": "Aan map toevoegen" + }, + "selectFolder": { + "message": "Map selecteren" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ worden permanent overgedragen aan de geselecteerde organisatie. Je bent niet langer de eigenaar van deze items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ wordt permanent overgedragen aan $ORG$. Je bent niet langer de eigenaar van deze items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/nn/messages.json b/apps/web/src/locales/nn/messages.json index faae698419f..cc07d6e9b82 100644 --- a/apps/web/src/locales/nn/messages.json +++ b/apps/web/src/locales/nn/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "E-postadresse" }, - "yourVaultIsLocked": { - "message": "Kvelvet ditt er låst. Stadfesta hovudpassordet ditt for å halda fram." + "yourVaultIsLockedV2": { + "message": "Your vault is locked" }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Are you sure you want to continue?" }, "moveSelectedItemsDesc": { - "message": "Velg ei mappe som du vil flytta $COUNT$ markert(e) oppføring(ar) til.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Scan the QR code below with your authenticator app or enter the key." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Could not load QR code. Try again or use the key below." + }, "key": { "message": "Nykel" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "Account email verified" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerifiedFailed": { "message": "Unable to verify your email. Try sending a new verification email." }, @@ -7877,7 +7883,7 @@ "message": "Assign to these collections" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Select collections to assign" @@ -8439,6 +8445,9 @@ "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." }, + "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." + }, "higherKDFIterations": { "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." }, @@ -8524,5 +8533,47 @@ "noInvoicesToList": { "message": "There are no invoices to list", "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,", + "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": { + "message": "contact Bitwarden support.", + "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'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsored" + }, + "licenseAndBillingManagementDesc": { + "message": "After making updates in the Bitwarden cloud server, upload your license file to apply the most recent changes." + }, + "addToFolder": { + "message": "Add to folder" + }, + "selectFolder": { + "message": "Select folder" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/or/messages.json b/apps/web/src/locales/or/messages.json index bec3ddfbae9..2d8503f35b8 100644 --- a/apps/web/src/locales/or/messages.json +++ b/apps/web/src/locales/or/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "Email address" }, - "yourVaultIsLocked": { - "message": "Your vault is locked. Verify your master password to continue." + "yourVaultIsLockedV2": { + "message": "Your vault is locked" }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Are you sure you want to continue?" }, "moveSelectedItemsDesc": { - "message": "Choose a folder that you would like to move the $COUNT$ selected item(s) to.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Scan the QR code below with your authenticator app or enter the key." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Could not load QR code. Try again or use the key below." + }, "key": { "message": "Key" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "Account email verified" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerifiedFailed": { "message": "Unable to verify your email. Try sending a new verification email." }, @@ -7877,7 +7883,7 @@ "message": "Assign to these collections" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Select collections to assign" @@ -8439,6 +8445,9 @@ "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." }, + "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." + }, "higherKDFIterations": { "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." }, @@ -8524,5 +8533,47 @@ "noInvoicesToList": { "message": "There are no invoices to list", "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,", + "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": { + "message": "contact Bitwarden support.", + "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'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsored" + }, + "licenseAndBillingManagementDesc": { + "message": "After making updates in the Bitwarden cloud server, upload your license file to apply the most recent changes." + }, + "addToFolder": { + "message": "Add to folder" + }, + "selectFolder": { + "message": "Select folder" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/pl/messages.json b/apps/web/src/locales/pl/messages.json index d942d916f83..4b33f28ab74 100644 --- a/apps/web/src/locales/pl/messages.json +++ b/apps/web/src/locales/pl/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "Adres e-mail" }, - "yourVaultIsLocked": { - "message": "Sejf jest zablokowany. Wpisz hasło główne, aby kontynuować." + "yourVaultIsLockedV2": { + "message": "Twój sejf jest zablokowany." }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Czy na pewno chcesz kontynuować?" }, "moveSelectedItemsDesc": { - "message": "Wybierz folder do którego chcesz przenieść zaznaczone elementy.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Zeskanuj poniższy kod QR za pomocą aplikacji uwierzytelniającej lub wprowadź klucz." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Nie można wyczytać kodu QR. Spróbuj ponownie lub użyj poniższego klucza." + }, "key": { "message": "Klucz" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "Adres e-mail został zweryfikowany" }, + "emailVerifiedV2": { + "message": "E-mail zweryfikowany" + }, "emailVerifiedFailed": { "message": "Nie możemy zweryfikować Twojego adresu e-mail. Spróbuj ponownie wysłać wiadomość weryfikacyjną." }, @@ -7877,7 +7883,7 @@ "message": "Przypisz do tych kolekcji" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Wybierz kolekcje, z którymi elementy będą udostępniane. Gdy element zostanie zaktualizowany w jednej kolekcji, zostanie to odzwierciedlone we wszystkich kolekcjach. Tylko członkowie organizacji z dostępem do tych kolekcji będą mogli zobaczyć te elementy." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Wybierz kolekcje do przypisania" @@ -8439,6 +8445,9 @@ "memberAccessReportDesc": { "message": "Upewnij się, że członkowie mają dostęp do odpowiednich danych uwierzytelniających, a ich konta są bezpieczne. Użyj tego raportu, aby uzyskać dostęp do CSV z dostępem użytkownika i konfiguracją konta." }, + "memberAccessReportPageDesc": { + "message": "Dostęp członków organizacji audytowej do wszystkich grup, kolekcji i elementów kolekcji. Eksport CSV zapewnia szczegółowy podział na członków, w tym informacje o uprawnieniach do zbierania i konfiguracjach kont." + }, "higherKDFIterations": { "message": "Wyższe wartości iteracji KDF mogą pomóc chronić Twoje hasło główne przed złamaniem przez atakującego." }, @@ -8524,5 +8533,47 @@ "noInvoicesToList": { "message": "Brak faktur do wyświetlenia", "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": "Uwaga: później w tym miesiącu poprawiona zostanie prywatność sejfu klienta, a członkowie dostawcy nie będą już mieli bezpośredniego dostępu do elementów sejfu klienta. Na pytania,", + "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": { + "message": "skontaktuj się z pomocą techniczną Bitwarden.", + "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'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsorowane" + }, + "licenseAndBillingManagementDesc": { + "message": "Po dokonaniu aktualizacji na serwerze w chmurze Bitwarden, prześlij plik licencyjny, aby zastosować najnowsze zmiany." + }, + "addToFolder": { + "message": "Dodaj do folderu" + }, + "selectFolder": { + "message": "Wybierz folder" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ zostanie trwale przeniesiony do wybranej organizacji. Nie będziesz już posiadać tych elementów.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ zostanie trwale przeniesiony do $ORG$. Nie będziesz już właścicielem tych elementów.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/pt_BR/messages.json b/apps/web/src/locales/pt_BR/messages.json index e630663835c..72240aa94a5 100644 --- a/apps/web/src/locales/pt_BR/messages.json +++ b/apps/web/src/locales/pt_BR/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "Endereço de e-mail" }, - "yourVaultIsLocked": { - "message": "O seu cofre está bloqueado. Verifique a sua senha mestra para continuar." + "yourVaultIsLockedV2": { + "message": "Your vault is locked" }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Tem certeza que deseja continuar?" }, "moveSelectedItemsDesc": { - "message": "Escolha uma pasta para a qual você deseja mover os $COUNT$ itens selecionados.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Scan the QR code below with your authenticator app or enter the key." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Could not load QR code. Try again or use the key below." + }, "key": { "message": "Chave" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "O seu e-mail foi verificado." }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerifiedFailed": { "message": "Não é possível confirmar o seu e-mail. Tente enviar um novo e-mail de verificação." }, @@ -7877,7 +7883,7 @@ "message": "Atribuir a estas coleções" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Selecione as coleções com as quais os itens serão compartilhados. Assim que um item for atualizado em uma coleção, isso será refletido em todas as coleções. Apenas membros da organização com acesso a essas coleções poderão ver os itens." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Selecione as coleções para atribuir" @@ -8439,6 +8445,9 @@ "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." }, + "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." + }, "higherKDFIterations": { "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." }, @@ -8524,5 +8533,47 @@ "noInvoicesToList": { "message": "There are no invoices to list", "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,", + "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": { + "message": "contact Bitwarden support.", + "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'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsored" + }, + "licenseAndBillingManagementDesc": { + "message": "After making updates in the Bitwarden cloud server, upload your license file to apply the most recent changes." + }, + "addToFolder": { + "message": "Add to folder" + }, + "selectFolder": { + "message": "Select folder" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/pt_PT/messages.json b/apps/web/src/locales/pt_PT/messages.json index e535189a189..200bac58516 100644 --- a/apps/web/src/locales/pt_PT/messages.json +++ b/apps/web/src/locales/pt_PT/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "Endereço de e-mail" }, - "yourVaultIsLocked": { - "message": "O seu cofre está bloqueado. Verifique a sua palavra-passe mestra para continuar." + "yourVaultIsLockedV2": { + "message": "O seu cofre está bloqueado." }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Tem a certeza de que deseja continuar?" }, "moveSelectedItemsDesc": { - "message": "Escolha uma pasta para a qual pretende mover o(s) $COUNT$ item(ns) selecionado(s).", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Digitalize o código QR abaixo com a sua aplicação de autenticação ou introduza a chave." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Não foi possível carregar o código QR. Tente novamente ou utilize a chave abaixo." + }, "key": { "message": "Chave" }, @@ -2671,7 +2674,7 @@ "message": "Sair" }, "leaveOrganizationConfirmation": { - "message": "Tem a certeza de que pretende deixar esta organização?" + "message": "Tem a certeza de que pretende sair desta organização?" }, "leftOrganization": { "message": "Saiu da organização" @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "E-mail da conta verificado" }, + "emailVerifiedV2": { + "message": "E-mail verificado" + }, "emailVerifiedFailed": { "message": "Não foi possível verificar o seu e-mail. Tente enviar um novo e-mail de verificação." }, @@ -4807,7 +4813,7 @@ "message": "Para verificar a sua 2FA, por favor, clique no botão abaixo." }, "webAuthnAuthenticate": { - "message": "Autenticar WebAuthn" + "message": "Autenticar o WebAuthn" }, "webAuthnNotSupported": { "message": "O WebAuthn não é suportado por este navegador." @@ -5526,7 +5532,7 @@ } }, "leaveOrganization": { - "message": "Deixar a organização" + "message": "Sair da organização" }, "removeMasterPassword": { "message": "Remover palavra-passe mestra" @@ -7877,7 +7883,7 @@ "message": "Atribuir a estas coleções" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Selecione as coleções com as quais os itens serão partilhados. Assim que um item for atualizado numa coleção, será refletido em todas as coleções. Apenas os membros da organização com acesso a estas coleções poderão ver os itens." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Selecione as coleções a atribuir" @@ -8439,6 +8445,9 @@ "memberAccessReportDesc": { "message": "Certifique-se de que os membros têm acesso às credenciais corretas e que as suas contas estão seguras. Utilize este relatório para obter um CSV das configurações de acesso e de contas dos membros." }, + "memberAccessReportPageDesc": { + "message": "Audite o acesso dos membros da organização a grupos, coleções e itens de coleção. A exportação CSV fornece uma análise detalhada por membro, incluindo informações sobre permissões de coleção e configurações de conta." + }, "higherKDFIterations": { "message": "Iterações KDF mais altas podem ajudar a proteger a sua palavra-passe mestra de ser forçada por um atacante." }, @@ -8524,5 +8533,47 @@ "noInvoicesToList": { "message": "Não há faturas a enumerar", "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": "Aviso: No final deste mês, a privacidade do cofre do cliente será melhorada e os membros do fornecedor deixarão de ter acesso direto aos itens do cofre do cliente. Para questões,", + "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": { + "message": "contacte o suporte do Bitwarden.", + "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'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Patrocinado" + }, + "licenseAndBillingManagementDesc": { + "message": "Depois de fazer atualizações no servidor de nuvem Bitwarden, carregue o seu ficheiro de licença para aplicar as alterações mais recentes." + }, + "addToFolder": { + "message": "Adicionar à pasta" + }, + "selectFolder": { + "message": "Selecionar pasta" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ serão permanentemente transferidos para a organização selecionada. Estes itens deixarão de lhe pertencer.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ serão permanentemente transferidos para $ORG$. Deixará de ser proprietário destes itens.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/ro/messages.json b/apps/web/src/locales/ro/messages.json index b5da535a0fb..d31aae4c89e 100644 --- a/apps/web/src/locales/ro/messages.json +++ b/apps/web/src/locales/ro/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "Adresă de e-mail" }, - "yourVaultIsLocked": { - "message": "Seiful dvs. este blocat. Verificați parola principală pentru a continua." + "yourVaultIsLockedV2": { + "message": "Your vault is locked" }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Are you sure you want to continue?" }, "moveSelectedItemsDesc": { - "message": "Alegeți un dosar în care doriți să mutați $COUNT$ articole selectate.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Scan the QR code below with your authenticator app or enter the key." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Could not load QR code. Try again or use the key below." + }, "key": { "message": "Cheie" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "E-mail cont verificat" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerifiedFailed": { "message": "E-mailul dvs. nu a putut fi verificat. Încercați să trimiteți un nou e-mail de verificare." }, @@ -7877,7 +7883,7 @@ "message": "Assign to these collections" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Select collections to assign" @@ -8439,6 +8445,9 @@ "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." }, + "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." + }, "higherKDFIterations": { "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." }, @@ -8524,5 +8533,47 @@ "noInvoicesToList": { "message": "There are no invoices to list", "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,", + "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": { + "message": "contact Bitwarden support.", + "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'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsored" + }, + "licenseAndBillingManagementDesc": { + "message": "After making updates in the Bitwarden cloud server, upload your license file to apply the most recent changes." + }, + "addToFolder": { + "message": "Add to folder" + }, + "selectFolder": { + "message": "Select folder" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/ru/messages.json b/apps/web/src/locales/ru/messages.json index 4a0dcf3e1f7..482cd72e25f 100644 --- a/apps/web/src/locales/ru/messages.json +++ b/apps/web/src/locales/ru/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "Адрес email" }, - "yourVaultIsLocked": { - "message": "Ваше хранилище заблокировано. Для продолжения введите мастер-пароль." + "yourVaultIsLockedV2": { + "message": "Ваше хранилище заблокировано." }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Вы действительно хотите продолжить?" }, "moveSelectedItemsDesc": { - "message": "Выберите папку, в которую вы хотите переместить выбранные элементы ($COUNT$ шт.).", + "message": "Выберите папку, в которую вы хотите добавить выбранные элементы ($COUNT$).", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Сосканируйте приведенный ниже QR-код с помощью приложения-аутентификатора или введите ключ." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Не удалось загрузить QR-код. Попробуйте еще раз или воспользуйтесь ключом ниже." + }, "key": { "message": "Ключ" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "Адрес email аккаунта подтвержден" }, + "emailVerifiedV2": { + "message": "Email подтвержден" + }, "emailVerifiedFailed": { "message": "Не удалось подтвердить ваш email. Попробуйте отправить новое письмо с подтверждением." }, @@ -7877,7 +7883,7 @@ "message": "Назначить этим коллекциям" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Выберите коллекции, в которые будут переданы элементы. Если элемент обновлен в одной коллекции, это изменение будет отражено во всех коллекциях. Только члены организации, имеющие доступ к этим коллекциям, смогут видеть элементы." + "message": "Только члены организации, имеющие доступ к этим коллекциям, смогут видеть элементы." }, "selectCollectionsToAssign": { "message": "Выбрать коллекции для назначения" @@ -8439,6 +8445,9 @@ "memberAccessReportDesc": { "message": "Убедитесь, что пользователи имеют доступ к необходимым им учетным данным, а их аккаунты надежно защищены. Используйте этот отчет, чтобы получить CSV-файл с данными о доступе пользователей и конфигурациях аккаунтов." }, + "memberAccessReportPageDesc": { + "message": "Аудит доступа членов организации к группам, коллекциям и элементам коллекций. Экспорт в формате CSV содержит подробную разбивку по членам, включая информацию о разрешениях на коллекции и конфигурациях учетных записей." + }, "higherKDFIterations": { "message": "Увеличение числа итераций KDF может помочь защитить ваш мастер-пароль от взлома его злоумышленником." }, @@ -8524,5 +8533,47 @@ "noInvoicesToList": { "message": "Нет счетов", "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": "Уведомление: позднее в этом месяце конфиденциальность клиентского хранилища будет улучшена, и провайдеры больше не будут иметь прямого доступа к элементам клиентского хранилища. По вопросам", + "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": { + "message": "обращайтесь в службу поддержки Bitwarden.", + "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'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Спонсировано" + }, + "licenseAndBillingManagementDesc": { + "message": "После обновления на сервере Bitwarden загрузите файл лицензии для применения последних изменений." + }, + "addToFolder": { + "message": "Добавить в папку" + }, + "selectFolder": { + "message": "Выбрать папку" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ будут навсегда переданы выбранной организации. Вы больше не будете владельцем этих элементов.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ будут навсегда переданы $ORG$. Вы больше не будете владельцем этих элементов.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/si/messages.json b/apps/web/src/locales/si/messages.json index 57ef40607e8..308eeaebe68 100644 --- a/apps/web/src/locales/si/messages.json +++ b/apps/web/src/locales/si/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "වි-තැපැල් ලිපිනය" }, - "yourVaultIsLocked": { - "message": "Your vault is locked. Verify your master password to continue." + "yourVaultIsLockedV2": { + "message": "Your vault is locked" }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Are you sure you want to continue?" }, "moveSelectedItemsDesc": { - "message": "Choose a folder that you would like to move the $COUNT$ selected item(s) to.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Scan the QR code below with your authenticator app or enter the key." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Could not load QR code. Try again or use the key below." + }, "key": { "message": "Key" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "Account email verified" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerifiedFailed": { "message": "Unable to verify your email. Try sending a new verification email." }, @@ -7877,7 +7883,7 @@ "message": "Assign to these collections" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Select collections to assign" @@ -8439,6 +8445,9 @@ "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." }, + "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." + }, "higherKDFIterations": { "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." }, @@ -8524,5 +8533,47 @@ "noInvoicesToList": { "message": "There are no invoices to list", "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,", + "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": { + "message": "contact Bitwarden support.", + "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'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsored" + }, + "licenseAndBillingManagementDesc": { + "message": "After making updates in the Bitwarden cloud server, upload your license file to apply the most recent changes." + }, + "addToFolder": { + "message": "Add to folder" + }, + "selectFolder": { + "message": "Select folder" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/sk/messages.json b/apps/web/src/locales/sk/messages.json index 99fe7fc2962..ab0dc33a0ba 100644 --- a/apps/web/src/locales/sk/messages.json +++ b/apps/web/src/locales/sk/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "Emailová adresa" }, - "yourVaultIsLocked": { - "message": "Váš trezor je uzamknutý. Overte sa hlavným heslom ak chcete pokračovať." + "yourVaultIsLockedV2": { + "message": "Váš trezor je zamknutý." }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Ste si istí, že chcete pokračovať?" }, "moveSelectedItemsDesc": { - "message": "Vyberte priečinok do ktorého chcete presunúť $COUNT$ vybraných položiek.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "S vašou overovacou aplikáciou skenujte QR kód nižšie, alebo ručne zadajte kľúč." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Nepodarilo sa načítať QR kód. Skúste to znova, alebo použite kľúč nižšie." + }, "key": { "message": "Kľúč" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "Emailová adresa konta bola overená" }, + "emailVerifiedV2": { + "message": "Email bol overený" + }, "emailVerifiedFailed": { "message": "Overovanie zlyhalo. Skúste si odoslať nový verifikačný e-mail." }, @@ -7877,7 +7883,7 @@ "message": "Prideliť k týmto zbierkam" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Vyberte zbierky s ktorými budú položky zdieľané. Zmeny položky v jednej zbierke sa prejavia vo všetkých zbierkach. Iba členovia organizácie s prístupom k týmto zbierkam budu položky vidieť." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Vyberte zbierky na pridelenie" @@ -8439,6 +8445,9 @@ "memberAccessReportDesc": { "message": "Uistite sa, že členovia majú prístup k správnym prístupovým údajom a ich kontá sú bezpečné. Prostredníctvom tohto reportu získajte CSV o prístupe a konfigurácii členov." }, + "memberAccessReportPageDesc": { + "message": "Audit prístupu členov organizácie v skupinách, kolekciách a položkách kolekcie. Export CSV poskytuje podrobný rozpis podľa jednotlivých členov vrátane informácií o oprávneniach kolekcií a konfiguráciách účtov." + }, "higherKDFIterations": { "message": "Zvýšenie počtu KDF iterácií môže pomôcť chrániť vaše hlavné heslo pri brute force útoku." }, @@ -8524,5 +8533,47 @@ "noInvoicesToList": { "message": "Neexistujú žiadne faktúry na zobrazenie", "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": "Upozornenie: Koncom tohto mesiaca sa zlepší ochrana osobných údajov v trezore klienta a členovia poskytovateľa už nebudú mať priamy prístup k položkám v trezore klienta. V prípade otázok", + "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": { + "message": "kontaktujte podporu spoločnosti Bitwarden.", + "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'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponzorované" + }, + "licenseAndBillingManagementDesc": { + "message": "Po vykonaní aktualizácií na cloudovom serveri Bitwarden nahrajte svoj licenčný súbor, aby ste aplikovali najnovšie zmeny." + }, + "addToFolder": { + "message": "Add to folder" + }, + "selectFolder": { + "message": "Select folder" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/sl/messages.json b/apps/web/src/locales/sl/messages.json index 17dc8995a34..85eeb907ddc 100644 --- a/apps/web/src/locales/sl/messages.json +++ b/apps/web/src/locales/sl/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "E-poštni naslov" }, - "yourVaultIsLocked": { - "message": "Vaš trezor je zaklenjen. Potrdite vaše glavno geslo za nadaljevanje." + "yourVaultIsLockedV2": { + "message": "Your vault is locked" }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Ste prepričani, da želite nadaljevati?" }, "moveSelectedItemsDesc": { - "message": "Izberite mapo, v katero bi radi premaknili teh $COUNT$ elementov.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Scan the QR code below with your authenticator app or enter the key." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Could not load QR code. Try again or use the key below." + }, "key": { "message": "Ključ" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "Account email verified" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerifiedFailed": { "message": "Unable to verify your email. Try sending a new verification email." }, @@ -7877,7 +7883,7 @@ "message": "Assign to these collections" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Select collections to assign" @@ -8439,6 +8445,9 @@ "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." }, + "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." + }, "higherKDFIterations": { "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." }, @@ -8524,5 +8533,47 @@ "noInvoicesToList": { "message": "There are no invoices to list", "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,", + "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": { + "message": "contact Bitwarden support.", + "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'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsored" + }, + "licenseAndBillingManagementDesc": { + "message": "After making updates in the Bitwarden cloud server, upload your license file to apply the most recent changes." + }, + "addToFolder": { + "message": "Add to folder" + }, + "selectFolder": { + "message": "Select folder" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/sr/messages.json b/apps/web/src/locales/sr/messages.json index 626871880bf..9453c73567d 100644 --- a/apps/web/src/locales/sr/messages.json +++ b/apps/web/src/locales/sr/messages.json @@ -174,7 +174,7 @@ "description": "This is the folder for uncategorized items" }, "selfOwnershipLabel": { - "message": "You", + "message": "Ти", "description": "Used as a label to indicate that the user is the owner of an item." }, "addFolder": { @@ -406,13 +406,13 @@ "message": "Ставка" }, "itemDetails": { - "message": "Item details" + "message": "Детаљи ставке" }, "itemName": { - "message": "Item name" + "message": "Име ставке" }, "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "message": "Не можете уклонити колекције са дозволама само за приказ: $COLLECTIONS$", "placeholders": { "collections": { "content": "$1", @@ -854,8 +854,8 @@ "emailAddress": { "message": "Имејл" }, - "yourVaultIsLocked": { - "message": "Сеф је блокиран. Унесите главну лозинку за наставак." + "yourVaultIsLockedV2": { + "message": "Your vault is locked" }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Желите ли заиста да наставите?" }, "moveSelectedItemsDesc": { - "message": "Изаберите фасциклу у коју желите да преместите одабране $COUNT$ ставке.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Скенирајте КР кôд у наставку помоћу апликације за аутентификацију или унесите кључ." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Could not load QR code. Try again or use the key below." + }, "key": { "message": "Кључ" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "Ваш имејл је потврђен." }, + "emailVerifiedV2": { + "message": "Имејл верификован" + }, "emailVerifiedFailed": { "message": "Није могуће верификовати ваш имејл. Покушајте да пошаљете нову поруку за верификацију." }, @@ -7877,7 +7883,7 @@ "message": "Додели овим колекцијама" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Изаберите колекције са којима ће се ставке делити. Када се ставка ажурира у једној колекцији, она ће се одразити на све колекције. Само чланови организације са приступом овим колекцијама ће моћи да виде ставке." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Изаберите колекције за доделу" @@ -8439,6 +8445,9 @@ "memberAccessReportDesc": { "message": "Уверите се да чланови имају приступ правим акредитивима и да су њихови налози сигурни. Користите овај извештај да бисте добили ЦСВ приступ чланова и конфигурације налога." }, + "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." + }, "higherKDFIterations": { "message": "Веће KDF итерације може помоћи у заштити ваше главне лозинке од грубе присиле од стране нападача." }, @@ -8522,7 +8531,49 @@ "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": { - "message": "There are no invoices to list", + "message": "Нема фактура за попис", "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": "Обавештење: Касније овог месеца, приватност сефа клијента ће бити побољшана и чланови провајдера више неће имати директан приступ ставкама клијентског сефа. За питања,", + "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": { + "message": "контактирајте подршку Bitwarden-а.", + "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'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsored" + }, + "licenseAndBillingManagementDesc": { + "message": "After making updates in the Bitwarden cloud server, upload your license file to apply the most recent changes." + }, + "addToFolder": { + "message": "Add to folder" + }, + "selectFolder": { + "message": "Select folder" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/sr_CS/messages.json b/apps/web/src/locales/sr_CS/messages.json index 10249636e2e..b5cc6b22a28 100644 --- a/apps/web/src/locales/sr_CS/messages.json +++ b/apps/web/src/locales/sr_CS/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "Imejl Adresa" }, - "yourVaultIsLocked": { - "message": "Vaš trezor je zaključan. Unesite glavnu lozinku da biste nastavili." + "yourVaultIsLockedV2": { + "message": "Your vault is locked" }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Are you sure you want to continue?" }, "moveSelectedItemsDesc": { - "message": "Choose a folder that you would like to move the $COUNT$ selected item(s) to.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Scan the QR code below with your authenticator app or enter the key." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Could not load QR code. Try again or use the key below." + }, "key": { "message": "Ključ" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "Account email verified" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerifiedFailed": { "message": "Unable to verify your email. Try sending a new verification email." }, @@ -7877,7 +7883,7 @@ "message": "Assign to these collections" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Select collections to assign" @@ -8439,6 +8445,9 @@ "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." }, + "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." + }, "higherKDFIterations": { "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." }, @@ -8524,5 +8533,47 @@ "noInvoicesToList": { "message": "There are no invoices to list", "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,", + "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": { + "message": "contact Bitwarden support.", + "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'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsored" + }, + "licenseAndBillingManagementDesc": { + "message": "After making updates in the Bitwarden cloud server, upload your license file to apply the most recent changes." + }, + "addToFolder": { + "message": "Add to folder" + }, + "selectFolder": { + "message": "Select folder" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/sv/messages.json b/apps/web/src/locales/sv/messages.json index 9c82c74b41d..b3692022bd9 100644 --- a/apps/web/src/locales/sv/messages.json +++ b/apps/web/src/locales/sv/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "E-postadress" }, - "yourVaultIsLocked": { - "message": "Valvet är låst. Bekräfta ditt huvudlösenord för att fortsätta." + "yourVaultIsLockedV2": { + "message": "Ditt valv är låst." }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Är du säker på att du vill fortsätta?" }, "moveSelectedItemsDesc": { - "message": "Välj en mapp som du vill flytta de $COUNT$ markerade objekten till.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Scan the QR code below with your authenticator app or enter the key." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Could not load QR code. Try again or use the key below." + }, "key": { "message": "Nyckel" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "E-postadressen har verifierats" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerifiedFailed": { "message": "Det gick inte att verifiera din e-postadress. Prova att skicka ett nytt verifieringsmeddelande." }, @@ -7877,7 +7883,7 @@ "message": "Assign to these collections" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Välj samlingar att tilldela" @@ -8439,6 +8445,9 @@ "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." }, + "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." + }, "higherKDFIterations": { "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." }, @@ -8524,5 +8533,47 @@ "noInvoicesToList": { "message": "There are no invoices to list", "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,", + "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": { + "message": "contact Bitwarden support.", + "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'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsored" + }, + "licenseAndBillingManagementDesc": { + "message": "After making updates in the Bitwarden cloud server, upload your license file to apply the most recent changes." + }, + "addToFolder": { + "message": "Lägg till i mapp" + }, + "selectFolder": { + "message": "Välj mapp" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/te/messages.json b/apps/web/src/locales/te/messages.json index bec3ddfbae9..2d8503f35b8 100644 --- a/apps/web/src/locales/te/messages.json +++ b/apps/web/src/locales/te/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "Email address" }, - "yourVaultIsLocked": { - "message": "Your vault is locked. Verify your master password to continue." + "yourVaultIsLockedV2": { + "message": "Your vault is locked" }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Are you sure you want to continue?" }, "moveSelectedItemsDesc": { - "message": "Choose a folder that you would like to move the $COUNT$ selected item(s) to.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Scan the QR code below with your authenticator app or enter the key." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Could not load QR code. Try again or use the key below." + }, "key": { "message": "Key" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "Account email verified" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerifiedFailed": { "message": "Unable to verify your email. Try sending a new verification email." }, @@ -7877,7 +7883,7 @@ "message": "Assign to these collections" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Select collections to assign" @@ -8439,6 +8445,9 @@ "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." }, + "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." + }, "higherKDFIterations": { "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." }, @@ -8524,5 +8533,47 @@ "noInvoicesToList": { "message": "There are no invoices to list", "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,", + "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": { + "message": "contact Bitwarden support.", + "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'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsored" + }, + "licenseAndBillingManagementDesc": { + "message": "After making updates in the Bitwarden cloud server, upload your license file to apply the most recent changes." + }, + "addToFolder": { + "message": "Add to folder" + }, + "selectFolder": { + "message": "Select folder" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/th/messages.json b/apps/web/src/locales/th/messages.json index a04a84caf03..40b78db0099 100644 --- a/apps/web/src/locales/th/messages.json +++ b/apps/web/src/locales/th/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "ที่อยู่อีเมล์" }, - "yourVaultIsLocked": { - "message": "ตู้เซฟของคุณถูกล็อค ใส่รหัสผ่านหลักของคุณเพื่อดำเนินการต่อ" + "yourVaultIsLockedV2": { + "message": "Your vault is locked" }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Are you sure you want to continue?" }, "moveSelectedItemsDesc": { - "message": "Choose a folder that you would like to move the $COUNT$ selected item(s) to.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Scan the QR code below with your authenticator app or enter the key." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Could not load QR code. Try again or use the key below." + }, "key": { "message": "Key" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "Account email verified" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerifiedFailed": { "message": "Unable to verify your email. Try sending a new verification email." }, @@ -7877,7 +7883,7 @@ "message": "Assign to these collections" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Select collections to assign" @@ -8439,6 +8445,9 @@ "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." }, + "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." + }, "higherKDFIterations": { "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." }, @@ -8524,5 +8533,47 @@ "noInvoicesToList": { "message": "There are no invoices to list", "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,", + "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": { + "message": "contact Bitwarden support.", + "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'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsored" + }, + "licenseAndBillingManagementDesc": { + "message": "After making updates in the Bitwarden cloud server, upload your license file to apply the most recent changes." + }, + "addToFolder": { + "message": "Add to folder" + }, + "selectFolder": { + "message": "Select folder" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/tr/messages.json b/apps/web/src/locales/tr/messages.json index 6725115d714..cc2d4a7d4c4 100644 --- a/apps/web/src/locales/tr/messages.json +++ b/apps/web/src/locales/tr/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "E-posta adresi" }, - "yourVaultIsLocked": { - "message": "Kasanız kilitli. Devam etmek için ana parolanızı doğrulayın." + "yourVaultIsLockedV2": { + "message": "Your vault is locked" }, "uuid": { "message": "UUID" @@ -989,7 +989,7 @@ "message": "Yubico OTP security key" }, "yubiKeyDesc": { - "message": "Hesabınıza erişmek için YubiKey kullanabilirsiniz. YubiKey 4 serisi, 5 serisi ve NEO cihazlarıyla çalışır." + "message": "YubiKey 4, 5 veya NEO cihazı kullanın." }, "duoDescV2": { "message": "Enter a code generated by Duo Security.", @@ -1006,10 +1006,10 @@ "message": "FIDO U2F güvenlik anahtarı" }, "webAuthnTitle": { - "message": "FIDO2 WebAuthn" + "message": "Geçiş anahtarı" }, "webAuthnDesc": { - "message": "Hesabınıza erişmek için WebAuthn uyumlu bir güvenlik anahtarı kullanın." + "message": "Cihazınızın biyometri özelliğini veya FIDO2 uyumlu bir güvenlik anahtarı kullanın." }, "webAuthnMigrated": { "message": "(FIDO'dan taşındı)" @@ -1060,7 +1060,7 @@ "message": "Devam etmek istediğinizden emin misiniz?" }, "moveSelectedItemsDesc": { - "message": "Seçtiğiniz $COUNT$ kaydı taşımak istediğiniz klasörü seçin.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1613,10 +1613,10 @@ "description": "Two-step login providers such as YubiKey, Duo, Authenticator apps, Email, etc." }, "enable": { - "message": "Aç" + "message": "Etkinleştir" }, "enabled": { - "message": "Açıldı" + "message": "Etkinleştirildi" }, "restoreAccess": { "message": "Erişimi geri getir" @@ -1647,7 +1647,7 @@ "message": "Yönetebilir" }, "disable": { - "message": "Kapat" + "message": "Devre dışı bırak" }, "revokeAccess": { "message": "Erişimi iptal et" @@ -1683,7 +1683,7 @@ "message": "You are leaving Bitwarden and launching an external website in a new window." }, "twoStepContinueToBitwardenUrlTitle": { - "message": "Continue to bitwarden.com?" + "message": "bitwarden.com'a gitmek ister misiniz?" }, "twoStepContinueToBitwardenUrlDesc": { "message": "Bitwarden Authenticator allows you to store authenticator keys and generate TOTP codes for 2-step verification flows. Learn more on the bitwarden.com website." @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Scan the QR code below with your authenticator app or enter the key." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Could not load QR code. Try again or use the key below." + }, "key": { "message": "Anahtar" }, @@ -1704,7 +1707,7 @@ "message": "Bu iki aşamalı giriş sağlayıcısını kaptmak istediğinizden emin misiniz?" }, "twoStepDisabled": { - "message": "İki aşamalı giriş sağlayıcısı kapatıldı." + "message": "İki aşamalı giriş sağlayıcısı devre dışı bırakıldı." }, "twoFactorYubikeyAdd": { "message": "Hesabıma yeni bir YubiKey ekle" @@ -2899,7 +2902,7 @@ "message": "İki aşamalı giriş kaydedildi" }, "disabled2fa": { - "message": "İki aşamalı giriş kapatıldı" + "message": "İki aşamalı giriş devre dışı bırakıldı" }, "recovered2fa": { "message": "Hesap iki aşamalı girişten kurtarıldı." @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "Hesap e-postası doğrulandı" }, + "emailVerifiedV2": { + "message": "E-posta doğrulandı" + }, "emailVerifiedFailed": { "message": "E-posta hesabı doğrulanamadı. Yeniden doğrulama e-postası göndermeyi deneyin." }, @@ -5577,10 +5583,10 @@ "message": "\"TOA ve Anahtar Bağlayıcı Şifre Çözme ile Oturum Açma\" etkinleştirildi. Bu politika yalnızca Sahipler ve Yöneticiler için geçerli olacaktır." }, "enabledSso": { - "message": "SSO açıldı" + "message": "SSO etkinleştirildi" }, "disabledSso": { - "message": "SSO kapatıldı" + "message": "SSO etkinleştirildi" }, "enabledKeyConnector": { "message": "Key Connector etkinleştirildi" @@ -5655,10 +5661,10 @@ "message": "Faturalandırma Senkronizasyonu Anahtarını yenilemek, önceki anahtarı geçersiz kılar." }, "selfHostedServer": { - "message": "self-hosted" + "message": "şirket içinde barındırılan" }, "customEnvironment": { - "message": "Custom environment" + "message": "Özel ortam" }, "selfHostedBaseUrlHint": { "message": "Specify the base URL of your on-premises hosted Bitwarden installation. Example: https://bitwarden.company.com" @@ -5670,22 +5676,22 @@ "message": "You must add either the base Server URL or at least one custom environment." }, "apiUrl": { - "message": "API server URL" + "message": "API sunucusu URL'si" }, "webVaultUrl": { - "message": "Web vault server URL" + "message": "Web kasası sunucu URL'si" }, "identityUrl": { - "message": "Identity server URL" + "message": "Kimlik sunucusu URL'si" }, "notificationsUrl": { - "message": "Notifications server URL" + "message": "Bildirim sunucusu URL'si" }, "iconsUrl": { - "message": "Icons server URL" + "message": "Simge sunucusu URL'si" }, "environmentSaved": { - "message": "Environment URLs saved" + "message": "Ortam URL'leri kaydedildi" }, "selfHostingTitle": { "message": "Barındırılan" @@ -6058,7 +6064,7 @@ "message": "Cihaz doğrulama" }, "enableDeviceVerification": { - "message": "Cihaz doğrulamasını aç" + "message": "Cihaz doğrulamasını etkinleştir" }, "deviceVerificationDesc": { "message": "Tanınmayan bir cihazdan oturum açarken e-posta adresinize doğrulama kodları gönderilir" @@ -6214,7 +6220,7 @@ "message": "Duo'yu başlat" }, "turnOn": { - "message": "Aç" + "message": "Etkinleştir" }, "on": { "message": "Açık" @@ -7877,7 +7883,7 @@ "message": "Bu koleksiyonlara ata" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Öğelerin paylaşılacağı koleksiyonları seçin. Bir koleksiyondaki bir öğe güncellendiğinde tüm koleksiyonlara yansıtılacaktır. Öğeleri yalnızca bu koleksiyonlara erişimi olan kuruluş üyeleri görebilir." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Atanacak koleksiyonları seçin" @@ -7902,7 +7908,7 @@ } }, "items": { - "message": "Ögeler" + "message": "Kayıtlar" }, "assignedSeats": { "message": "Assigned seats" @@ -7938,7 +7944,7 @@ "message": "Subscription update failed" }, "trial": { - "message": "Trial", + "message": "Deneme", "description": "A subscription status label." }, "pastDue": { @@ -7946,7 +7952,7 @@ "description": "A subscription status label" }, "subscriptionExpired": { - "message": "Subscription expired", + "message": "Abonelik sona erdi", "description": "The date header used when a subscription is past due." }, "pastDueWarningForChargeAutomatically": { @@ -7986,7 +7992,7 @@ "description": "The body of a warning box shown to a user whose subscription is unpaid." }, "cancellationDate": { - "message": "Cancellation date", + "message": "İptal tarihi", "description": "The date header used when a subscription is cancelled." }, "machineAccountsCannotCreate": { @@ -8037,7 +8043,7 @@ "message": "Deleting machine accounts is permanent and irreversible." }, "deleteMachineAccountsConfirmMessage": { - "message": "Delete $COUNT$ machine accounts", + "message": "$COUNT$ makine hesabını sil", "placeholders": { "count": { "content": "$1", @@ -8046,60 +8052,60 @@ } }, "deleteMachineAccountToast": { - "message": "Machine account deleted" + "message": "Makine hesabı silindi" }, "deleteMachineAccountsToast": { - "message": "Machine accounts deleted" + "message": "Makine hesapları silindi" }, "searchMachineAccounts": { - "message": "Search machine accounts", + "message": "Makine hesaplarında ara", "description": "Placeholder text for searching machine accounts." }, "editMachineAccount": { - "message": "Edit machine account", + "message": "Makine hesabını düzenle", "description": "Title for editing a machine account." }, "machineAccountName": { - "message": "Machine account name", + "message": "Makine hesabı adı", "description": "Label for the name of a machine account" }, "machineAccountCreated": { - "message": "Machine account created", + "message": "Makine hesabı oluşturuldu", "description": "Notifies that a new machine account has been created" }, "machineAccountUpdated": { - "message": "Machine account updated", + "message": "Makine hesabı güncellendi", "description": "Notifies that a machine account has been updated" }, "projectMachineAccountsDescription": { - "message": "Grant machine accounts access to this project." + "message": "Makine hesaplarına bu projeye erişim izni verin." }, "projectMachineAccountsSelectHint": { - "message": "Type or select machine accounts" + "message": "Makine hesaplarını yazın veya seçin" }, "projectEmptyMachineAccountAccessPolicies": { - "message": "Add machine accounts to grant access" + "message": "Makine vermek için hizmet hesapları ekleyin" }, "machineAccountPeopleDescription": { - "message": "Grant groups or people access to this machine account." + "message": "Gruplara veya kişilere bu makine hesabına erişim izni verin." }, "machineAccountProjectsDescription": { - "message": "Assign projects to this machine account. " + "message": "Bu makine hesabına projeler atayın. " }, "createMachineAccount": { - "message": "Create a machine account" + "message": "Makine hesabı oluştur" }, "maPeopleWarningMessage": { "message": "Removing people from a machine account does not remove the access tokens they created. For security best practice, it is recommended to revoke access tokens created by people removed from a machine account." }, "smAccessRemovalWarningMaTitle": { - "message": "Remove access to this machine account" + "message": "Bu makine hesabına erişimi kaldır" }, "smAccessRemovalWarningMaMessage": { - "message": "This action will remove your access to the machine account." + "message": "Bu işlem, makine hesabına erişiminizi kaldıracaktır." }, "machineAccountsIncluded": { - "message": "$COUNT$ machine accounts included", + "message": "$COUNT$ makine hesabı dahil", "placeholders": { "count": { "content": "$1", @@ -8108,7 +8114,7 @@ } }, "additionalMachineAccountCost": { - "message": "$COST$ per month for additional machine accounts", + "message": "İlave makine hesapları için aylık $COST$", "placeholders": { "cost": { "content": "$1", @@ -8117,7 +8123,7 @@ } }, "additionalMachineAccounts": { - "message": "Additional machine accounts" + "message": "İlave makine hesapları" }, "includedMachineAccounts": { "message": "Your plan comes with $COUNT$ machine accounts.", @@ -8150,10 +8156,10 @@ "message": "Max potential machine account cost" }, "machineAccountAccessUpdated": { - "message": "Machine account access updated" + "message": "Makine hesabı erişimi güncellendi" }, "restrictedGroupAccessDesc": { - "message": "You cannot add yourself to a group." + "message": "Kendinizi gruba ekleyemezsiniz." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." @@ -8219,7 +8225,7 @@ "description": "The title for the section that deals with integrations and SDKs." }, "integrations": { - "message": "Integrations" + "message": "Entegrasyonlar" }, "integrationsDesc": { "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." @@ -8273,10 +8279,10 @@ "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." }, "selectAPlan": { - "message": "Select a plan" + "message": "Bir plan seçin" }, "thirtyFivePercentDiscount": { - "message": "35% İndirim" + "message": "35% indirim" }, "monthPerMember": { "message": "month per member" @@ -8285,13 +8291,13 @@ "message": "Seats" }, "addOrganization": { - "message": "Add organization" + "message": "Kuruluş ekle" }, "createdNewClient": { "message": "Successfully created new client" }, "noAccess": { - "message": "No access" + "message": "Erişim yok" }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" @@ -8329,11 +8335,11 @@ } }, "back": { - "message": "Back", + "message": "Geri", "description": "Button text to navigate back" }, "removeItem": { - "message": "Remove $NAME$", + "message": "$NAME$ klasörünü kaldır", "description": "Remove a selected option, such as a folder or collection", "placeholders": { "name": { @@ -8343,10 +8349,10 @@ } }, "viewInfo": { - "message": "View info" + "message": "Bilgileri görüntüle" }, "viewAccess": { - "message": "View access" + "message": "Erişimi görüntüle" }, "noCollectionsSelected": { "message": "You have not selected any collections." @@ -8364,7 +8370,7 @@ "message": "Organization Seats" }, "providerDiscount": { - "message": "%$AMOUNT$ İndirim", + "message": "%$AMOUNT$ indirim", "placeholders": { "amount": { "content": "$1", @@ -8406,10 +8412,10 @@ "message": "Updated tax information" }, "unverified": { - "message": "Unverified" + "message": "Doğrulanmadı" }, "verified": { - "message": "Verified" + "message": "Doğrulandı" }, "viewSecret": { "message": "View secret" @@ -8428,7 +8434,7 @@ "message": "Quickly view member access across the organization by upgrading to an Enterprise plan." }, "date": { - "message": "Date" + "message": "Tarih" }, "exportClientReport": { "message": "Export client report" @@ -8439,6 +8445,9 @@ "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." }, + "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." + }, "higherKDFIterations": { "message": "KDF iterasyonunun daha yüksek olması ana parolanızı kaba kuvvet saldırılarına karşı daha iyi korur." }, @@ -8509,7 +8518,7 @@ "message": "Client details" }, "downloadCSV": { - "message": "Download CSV" + "message": "CSV'yi indir" }, "monthlySubscriptionUserSeatsMessage": { "message": "Adjustments to your subscription will result in prorated charges to your billing totals on your next billing period. " @@ -8524,5 +8533,47 @@ "noInvoicesToList": { "message": "Listelenecek fatura yok", "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,", + "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": { + "message": "contact Bitwarden support.", + "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'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsorlu" + }, + "licenseAndBillingManagementDesc": { + "message": "After making updates in the Bitwarden cloud server, upload your license file to apply the most recent changes." + }, + "addToFolder": { + "message": "Klasöre ekle" + }, + "selectFolder": { + "message": "Klasör seç" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/uk/messages.json b/apps/web/src/locales/uk/messages.json index d2602ac38e4..76cefd8bfe5 100644 --- a/apps/web/src/locales/uk/messages.json +++ b/apps/web/src/locales/uk/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "Адреса е-пошти" }, - "yourVaultIsLocked": { - "message": "Сховище заблоковано. Введіть головний пароль для продовження." + "yourVaultIsLockedV2": { + "message": "Ваше сховище заблоковано." }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Ви дійсно хочете продовжити?" }, "moveSelectedItemsDesc": { - "message": "Оберіть теку, в яку ви бажаєте перемістити $COUNT$ вибраних записів.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Скануйте зазначений нижче QR-код за допомогою програми для автентифікації або введіть ключ." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Не вдалося завантажити QR-код. Повторіть спробу або скористайтеся зазначеним нижче ключем." + }, "key": { "message": "Ключ" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "Адресу е-пошти підтверджено" }, + "emailVerifiedV2": { + "message": "Електронну пошту підтверджено" + }, "emailVerifiedFailed": { "message": "Неможливо підтвердити вашу е-пошту. Спробуйте надіслати нове повідомлення для підтвердження." }, @@ -7877,7 +7883,7 @@ "message": "Призначити до цих збірок" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Оберіть збірки, в яких поширюватимуться записи. Після оновлення запису в одній збірці зміни буде відображено у всіх збірках. Лише учасники організації з доступом до цих збірок зможуть переглядати записи." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Оберіть збірки для призначення" @@ -8439,6 +8445,9 @@ "memberAccessReportDesc": { "message": "Переконайтеся, що учасники мають доступ до належних облікових даних і їхні облікові записи надійні. Використовуйте цей звіт, щоб отримати файл CSV з даними про доступ учасників та конфігурації облікових записів." }, + "memberAccessReportPageDesc": { + "message": "Аудит доступу учасника організації до груп, збірок та елементів збірок. Експорт CSV надає детальну розбивку для кожного учасника, зокрема інформацію про дозволи для збірок та конфігурації облікового запису." + }, "higherKDFIterations": { "message": "Вищі значення KDF-ітерацій можуть допомогти захистити ваш головний пароль від грубого зламу зловмисником." }, @@ -8524,5 +8533,47 @@ "noInvoicesToList": { "message": "Немає рахунків для показу", "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": "Повідомлення: пізніше цього місяця приватність клієнтського сховища буде поліпшено й учасники провайдера більше не матимуть прямого доступу до елементів клієнтських сховищ. Для отримання відповідей на запитання,", + "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": { + "message": "зверніться до служби підтримки Bitwarden.", + "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'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Спонсоровано" + }, + "licenseAndBillingManagementDesc": { + "message": "After making updates in the Bitwarden cloud server, upload your license file to apply the most recent changes." + }, + "addToFolder": { + "message": "Add to folder" + }, + "selectFolder": { + "message": "Select folder" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/vi/messages.json b/apps/web/src/locales/vi/messages.json index b82c86b1476..2969940f86e 100644 --- a/apps/web/src/locales/vi/messages.json +++ b/apps/web/src/locales/vi/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "Địa chỉ email" }, - "yourVaultIsLocked": { - "message": "Kho của bạn đã bị khóa. Nhập mật khẩu chính để tiếp tục." + "yourVaultIsLockedV2": { + "message": "Your vault is locked" }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Bạn có chắc rằng bạn muốn tiếp tục?" }, "moveSelectedItemsDesc": { - "message": "Chọn thư mục mà bạn muốn di chuyển $COUNT$ mục này tới.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Scan the QR code below with your authenticator app or enter the key." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Could not load QR code. Try again or use the key below." + }, "key": { "message": "Khóa" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "Account email verified" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerifiedFailed": { "message": "Unable to verify your email. Try sending a new verification email." }, @@ -7877,7 +7883,7 @@ "message": "Assign to these collections" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Select collections to assign" @@ -8439,6 +8445,9 @@ "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." }, + "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." + }, "higherKDFIterations": { "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." }, @@ -8524,5 +8533,47 @@ "noInvoicesToList": { "message": "There are no invoices to list", "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,", + "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": { + "message": "contact Bitwarden support.", + "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'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsored" + }, + "licenseAndBillingManagementDesc": { + "message": "After making updates in the Bitwarden cloud server, upload your license file to apply the most recent changes." + }, + "addToFolder": { + "message": "Add to folder" + }, + "selectFolder": { + "message": "Select folder" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/zh_CN/messages.json b/apps/web/src/locales/zh_CN/messages.json index e327eb35a93..8030c4986fc 100644 --- a/apps/web/src/locales/zh_CN/messages.json +++ b/apps/web/src/locales/zh_CN/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "电子邮件地址" }, - "yourVaultIsLocked": { - "message": "您的密码库已锁定,请验证您的主密码以继续。" + "yourVaultIsLockedV2": { + "message": "您的密码库已锁定" }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "确定要继续吗?" }, "moveSelectedItemsDesc": { - "message": "选择要将这 $COUNT$ 个项目移动到的文件夹。", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "用您的验证器 App 扫描下面的二维码或输入密钥。" }, + "twoStepAuthenticatorQRCanvasError": { + "message": "无法加载二维码。请重试或使用下面的密钥。" + }, "key": { "message": "密钥" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "账户电子邮件已验证" }, + "emailVerifiedV2": { + "message": "电子邮箱已验证" + }, "emailVerifiedFailed": { "message": "无法验证您的电子邮件。尝试发送新的验证电子邮件。" }, @@ -7877,7 +7883,7 @@ "message": "分配到这些集合" }, "bulkCollectionAssignmentDialogDescription": { - "message": "选择与其共享项目的集合。当一个项目在某个集合中更新后,它将反映到所有集合中。只有具有这些集合访问权限的组织成员才能看到这些项目。" + "message": "只有具有这些集合访问权限的组织成员才能看到这些项目。" }, "selectCollectionsToAssign": { "message": "选择要分配的集合" @@ -8439,6 +8445,9 @@ "memberAccessReportDesc": { "message": "确保成员具有对合适的凭据的访问权限,以及他们的账户是安全的。使用此报告获取包含会员访问权限和账户配置的 CSV 文件 。" }, + "memberAccessReportPageDesc": { + "message": "审计组织成员在各个群组、集合和集合项目之间的访问权限。CSV 导出文件提供了每位成员的详细信息,包括集合权限和账户配置的相关信息。" + }, "higherKDFIterations": { "message": "更高的 KDF 迭代可以帮助保护您的主密码免遭攻击者的暴力破解。" }, @@ -8524,5 +8533,47 @@ "noInvoicesToList": { "message": "没有可列出的账单", "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": "注意:本月晚些时候,客户密码库隐私将被改进,提供商成员将不再能够直接访问客户密码库项目。如有疑问,", + "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": { + "message": "联系 Bitwarden 支持。", + "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'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "赞助" + }, + "licenseAndBillingManagementDesc": { + "message": "在 Bitwarden 云服务器中进行更新后,上传许可证文件以应用最新的更改。" + }, + "addToFolder": { + "message": "添加到文件夹" + }, + "selectFolder": { + "message": "选择文件夹" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ 将永久转移到所选组织。您将不再拥有这些项目。", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ 将永久转移到 $ORG$。您将不再拥有这些项目。", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/zh_TW/messages.json b/apps/web/src/locales/zh_TW/messages.json index 1892eb0801a..2bd991eabb9 100644 --- a/apps/web/src/locales/zh_TW/messages.json +++ b/apps/web/src/locales/zh_TW/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "電子郵件地址" }, - "yourVaultIsLocked": { - "message": "密碼庫已鎖定。請驗證主密碼以繼續。" + "yourVaultIsLockedV2": { + "message": "Your vault is locked" }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "您確定要繼續嗎?" }, "moveSelectedItemsDesc": { - "message": "選擇要將這 $COUNT$ 個項目移動至哪個資料夾。", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Scan the QR code below with your authenticator app or enter the key." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Could not load QR code. Try again or use the key below." + }, "key": { "message": "金鑰" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "帳戶電子郵件已驗證" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerifiedFailed": { "message": "無法驗證電子郵件。請嘗試傳送一封新的驗證電子郵件。" }, @@ -7877,7 +7883,7 @@ "message": "Assign to these collections" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Select collections to assign" @@ -8439,6 +8445,9 @@ "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." }, + "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." + }, "higherKDFIterations": { "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." }, @@ -8524,5 +8533,47 @@ "noInvoicesToList": { "message": "There are no invoices to list", "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,", + "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": { + "message": "contact Bitwarden support.", + "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'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsored" + }, + "licenseAndBillingManagementDesc": { + "message": "After making updates in the Bitwarden cloud server, upload your license file to apply the most recent changes." + }, + "addToFolder": { + "message": "Add to folder" + }, + "selectFolder": { + "message": "Select folder" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } From 91dc9f49a53e4144a5676dd42f04887386fa6cee Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Fri, 12 Jul 2024 14:31:41 +0200 Subject: [PATCH 12/57] [PM-6188] Remove get bg service calls (#10050) * Remove ssologinservice getbgservice call * Remove deviceservice getbgservice call * Remove keyconnectorservice getbgservice call * Remove userverificationservice getbgservice call * Remove vaulttimeoutsettingservice getbgservice call * Remove devicetrustservice getbgservice call --- .../src/popup/services/services.module.ts | 35 ------------------- 1 file changed, 35 deletions(-) diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index e82eb429a5a..b504996e004 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -20,18 +20,13 @@ import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services. import { PinServiceAbstraction } from "@bitwarden/auth/common"; import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service"; import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service"; -import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { AccountService as AccountServiceAbstraction } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; -import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; -import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction"; import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; -import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; -import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { AutofillSettingsService, @@ -168,11 +163,6 @@ const safeProviders: SafeProvider[] = [ useClass: UnauthGuardService, deps: [AuthService, Router], }), - safeProvider({ - provide: SsoLoginServiceAbstraction, - useFactory: getBgService("ssoLoginService"), - deps: [], - }), safeProvider({ provide: CryptoFunctionService, useFactory: () => new WebCryptoFunctionService(window), @@ -255,16 +245,6 @@ const safeProviders: SafeProvider[] = [ useClass: TotpService, deps: [CryptoFunctionService, LogService], }), - safeProvider({ - provide: DeviceTrustServiceAbstraction, - useFactory: getBgService("deviceTrustService"), - deps: [], - }), - safeProvider({ - provide: DevicesServiceAbstraction, - useFactory: getBgService("devicesService"), - deps: [], - }), safeProvider({ provide: OffscreenDocumentService, useClass: DefaultOffscreenDocumentService, @@ -338,21 +318,6 @@ const safeProviders: SafeProvider[] = [ useClass: BrowserScriptInjectorService, deps: [PlatformUtilsService, LogService], }), - safeProvider({ - provide: KeyConnectorService, - useFactory: getBgService("keyConnectorService"), - deps: [], - }), - safeProvider({ - provide: UserVerificationService, - useFactory: getBgService("userVerificationService"), - deps: [], - }), - safeProvider({ - provide: VaultTimeoutSettingsService, - useFactory: getBgService("vaultTimeoutSettingsService"), - deps: [], - }), safeProvider({ provide: VaultTimeoutService, useFactory: getBgService("vaultTimeoutService"), From 486176a6483575375a3fbba091da702a1335fd8d Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Fri, 12 Jul 2024 14:32:01 +0200 Subject: [PATCH 13/57] Add foregroundvaulttimeout service and remove getbgservice call for vaulttimeout service (#10071) --- .../src/background/runtime.background.ts | 3 +++ .../src/popup/services/services.module.ts | 6 ++++-- .../foreground-vault-timeout.service.ts | 18 ++++++++++++++++++ 3 files changed, 25 insertions(+), 2 deletions(-) create mode 100644 apps/browser/src/services/vault-timeout/foreground-vault-timeout.service.ts diff --git a/apps/browser/src/background/runtime.background.ts b/apps/browser/src/background/runtime.background.ts index 94e96e2dc89..ccf82057449 100644 --- a/apps/browser/src/background/runtime.background.ts +++ b/apps/browser/src/background/runtime.background.ts @@ -233,6 +233,9 @@ export default class RuntimeBackground { case "addToLockedVaultPendingNotifications": this.lockedVaultPendingNotifications.push(msg.data); break; + case "lockVault": + await this.main.vaultTimeoutService.lock(msg.userId); + break; case "logout": await this.main.logout(msg.expired, msg.userId); break; diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index b504996e004..b083c2f4c8b 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -50,6 +50,7 @@ import { FileDownloadService } from "@bitwarden/common/platform/abstractions/fil import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service"; import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { MessagingService as MessagingServiceAbstraction } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { @@ -104,6 +105,7 @@ import { ForegroundPlatformUtilsService } from "../../platform/services/platform import { BrowserStorageServiceProvider } from "../../platform/storage/browser-storage-service.provider"; import { ForegroundMemoryStorageService } from "../../platform/storage/foreground-memory-storage.service"; import { fromChromeRuntimeMessaging } from "../../platform/utils/from-chrome-runtime-messaging"; +import { ForegroundVaultTimeoutService } from "../../services/vault-timeout/foreground-vault-timeout.service"; import { BrowserSendStateService } from "../../tools/popup/services/browser-send-state.service"; import { FilePopoutUtilsService } from "../../tools/popup/services/file-popout-utils.service"; import { Fido2UserVerificationService } from "../../vault/services/fido2-user-verification.service"; @@ -320,8 +322,8 @@ const safeProviders: SafeProvider[] = [ }), safeProvider({ provide: VaultTimeoutService, - useFactory: getBgService("vaultTimeoutService"), - deps: [], + useClass: ForegroundVaultTimeoutService, + deps: [MessagingServiceAbstraction], }), safeProvider({ provide: NotificationsService, diff --git a/apps/browser/src/services/vault-timeout/foreground-vault-timeout.service.ts b/apps/browser/src/services/vault-timeout/foreground-vault-timeout.service.ts new file mode 100644 index 00000000000..462e2149e88 --- /dev/null +++ b/apps/browser/src/services/vault-timeout/foreground-vault-timeout.service.ts @@ -0,0 +1,18 @@ +import { VaultTimeoutService as BaseVaultTimeoutService } from "@bitwarden/common/src/abstractions/vault-timeout/vault-timeout.service"; +import { MessagingService } from "@bitwarden/common/src/platform/abstractions/messaging.service"; +import { UserId } from "@bitwarden/common/src/types/guid"; + +export class ForegroundVaultTimeoutService implements BaseVaultTimeoutService { + constructor(protected messagingService: MessagingService) {} + + // should only ever run in background + async checkVaultTimeout(): Promise {} + + async lock(userId?: UserId): Promise { + this.messagingService.send("lockVault", { userId }); + } + + async logOut(userId?: string): Promise { + this.messagingService.send("logout", { userId }); + } +} From bce6e775146d76941ad5ec75fd8e41f7ae1989ad Mon Sep 17 00:00:00 2001 From: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com> Date: Fri, 12 Jul 2024 11:06:02 -0500 Subject: [PATCH 14/57] [AC-2520] Remove Unassigned Items Banner (#10042) * chore: remove UnassignedItemsBanner feature flag, refs AC-2520 * chore: remove unassignedItemsBanner from web-header component, refs AC-2520 * chore: delete unassigned items banner service/api/spec, refs AC-2520 * chore: remove unassigned items banner messages (web), refs AC-2520 * chore: remove unassigned items banner messages (browser), refs AC-2520 * chore: remove unassigned items banner code from current tab (browser), refs AC-2520 * chore: remove state definition for unassigned items banner, refs AC-2520 * chore: revert state-definition removal, refs AC-2520 --- apps/browser/src/_locales/en/messages.json | 14 --- .../vault/current-tab.component.html | 38 -------- .../components/vault/current-tab.component.ts | 9 -- .../layouts/header/web-header.component.html | 28 ------ .../layouts/header/web-header.component.ts | 8 -- apps/web/src/locales/en/messages.json | 17 ---- .../unassigned-items-banner.api.service.ts | 19 ---- .../unassigned-items-banner.service.spec.ts | 65 -------------- .../unassigned-items-banner.service.ts | 87 ------------------- libs/common/src/enums/feature-flag.enum.ts | 2 - 10 files changed, 287 deletions(-) delete mode 100644 libs/angular/src/services/unassigned-items-banner.api.service.ts delete mode 100644 libs/angular/src/services/unassigned-items-banner.service.spec.ts delete mode 100644 libs/angular/src/services/unassigned-items-banner.service.ts diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 22e0eba1b4f..5152028d377 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -3358,20 +3358,6 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Auto-fill suggestions" }, diff --git a/apps/browser/src/vault/popup/components/vault/current-tab.component.html b/apps/browser/src/vault/popup/components/vault/current-tab.component.html index 0b2e16d09d2..bb8a401da62 100644 --- a/apps/browser/src/vault/popup/components/vault/current-tab.component.html +++ b/apps/browser/src/vault/popup/components/vault/current-tab.component.html @@ -36,44 +36,6 @@ - -

- {{ unassignedItemsBannerService.bannerText$ | async | i18n }} - {{ "unassignedItemsBannerCTAPartOne" | i18n }} - {{ "adminConsole" | i18n }} - {{ "unassignedItemsBannerCTAPartTwo" | i18n }} - {{ "learnMore" | i18n }} -

- -

{{ "typeLogins" | i18n }} diff --git a/apps/browser/src/vault/popup/components/vault/current-tab.component.ts b/apps/browser/src/vault/popup/components/vault/current-tab.component.ts index 97856a952ce..ec69330745f 100644 --- a/apps/browser/src/vault/popup/components/vault/current-tab.component.ts +++ b/apps/browser/src/vault/popup/components/vault/current-tab.component.ts @@ -3,14 +3,11 @@ import { Router } from "@angular/router"; import { Subject, firstValueFrom, from, Subscription } from "rxjs"; import { debounceTime, switchMap, takeUntil } from "rxjs/operators"; -import { UnassignedItemsBannerService } from "@bitwarden/angular/services/unassigned-items-banner.service"; import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants"; import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; -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 { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; @@ -58,10 +55,6 @@ export class CurrentTabComponent implements OnInit, OnDestroy { private loadedTimeout: number; private searchTimeout: number; - protected unassignedItemsBannerEnabled$ = this.configService.getFeatureFlag$( - FeatureFlag.UnassignedItemsBanner, - ); - constructor( private platformUtilsService: PlatformUtilsService, private cipherService: CipherService, @@ -78,8 +71,6 @@ export class CurrentTabComponent implements OnInit, OnDestroy { private organizationService: OrganizationService, private vaultFilterService: VaultFilterService, private vaultSettingsService: VaultSettingsService, - private configService: ConfigService, - protected unassignedItemsBannerService: UnassignedItemsBannerService, ) {} async ngOnInit() { diff --git a/apps/web/src/app/layouts/header/web-header.component.html b/apps/web/src/app/layouts/header/web-header.component.html index e2b3e7910ab..5b55eede778 100644 --- a/apps/web/src/app/layouts/header/web-header.component.html +++ b/apps/web/src/app/layouts/header/web-header.component.html @@ -1,31 +1,3 @@ - - {{ unassignedItemsBannerService.bannerText$ | async | i18n }} - {{ "unassignedItemsBannerCTAPartOne" | i18n }} - {{ "adminConsole" | i18n }} - {{ "unassignedItemsBannerCTAPartTwo" | i18n }} - {{ "learnMore" | i18n }} -
; protected selfHosted: boolean; protected hostname = location.hostname; - protected unassignedItemsBannerEnabled$ = this.configService.getFeatureFlag$( - FeatureFlag.UnassignedItemsBanner, - ); constructor( private route: ActivatedRoute, private platformUtilsService: PlatformUtilsService, private vaultTimeoutSettingsService: VaultTimeoutSettingsService, private messagingService: MessagingService, - protected unassignedItemsBannerService: UnassignedItemsBannerService, - private configService: ConfigService, private accountService: AccountService, ) { this.routeData$ = this.route.data.pipe( diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index f5875601189..0f9e334feaa 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -8161,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "You cannot add yourself to a group." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Delete provider" }, diff --git a/libs/angular/src/services/unassigned-items-banner.api.service.ts b/libs/angular/src/services/unassigned-items-banner.api.service.ts deleted file mode 100644 index 69b74f8c7fa..00000000000 --- a/libs/angular/src/services/unassigned-items-banner.api.service.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Injectable } from "@angular/core"; - -import { ApiService } from "@bitwarden/common/abstractions/api.service"; - -@Injectable({ providedIn: "root" }) -export class UnassignedItemsBannerApiService { - constructor(private apiService: ApiService) {} - - async getShowUnassignedCiphersBanner(): Promise { - const r = await this.apiService.send( - "GET", - "/ciphers/has-unassigned-ciphers", - null, - true, - true, - ); - return r; - } -} diff --git a/libs/angular/src/services/unassigned-items-banner.service.spec.ts b/libs/angular/src/services/unassigned-items-banner.service.spec.ts deleted file mode 100644 index bf0fb23881c..00000000000 --- a/libs/angular/src/services/unassigned-items-banner.service.spec.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { MockProxy, mock } from "jest-mock-extended"; -import { firstValueFrom, of } from "rxjs"; - -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; -import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; -import { FakeStateProvider, mockAccountServiceWith } from "@bitwarden/common/spec"; -import { UserId } from "@bitwarden/common/types/guid"; - -import { UnassignedItemsBannerApiService } from "./unassigned-items-banner.api.service"; -import { SHOW_BANNER_KEY, UnassignedItemsBannerService } from "./unassigned-items-banner.service"; - -describe("UnassignedItemsBanner", () => { - let stateProvider: FakeStateProvider; - let apiService: MockProxy; - let environmentService: MockProxy; - let organizationService: MockProxy; - - const sutFactory = () => - new UnassignedItemsBannerService( - stateProvider, - apiService, - environmentService, - organizationService, - ); - - beforeEach(() => { - const fakeAccountService = mockAccountServiceWith("userId" as UserId); - stateProvider = new FakeStateProvider(fakeAccountService); - apiService = mock(); - environmentService = mock(); - environmentService.environment$ = of(null); - organizationService = mock(); - organizationService.organizations$ = of([]); - }); - - it("shows the banner if showBanner local state is true", async () => { - const showBanner = stateProvider.activeUser.getFake(SHOW_BANNER_KEY); - showBanner.nextState(true); - - const sut = sutFactory(); - expect(await firstValueFrom(sut.showBanner$)).toBe(true); - expect(apiService.getShowUnassignedCiphersBanner).not.toHaveBeenCalled(); - }); - - it("does not show the banner if showBanner local state is false", async () => { - const showBanner = stateProvider.activeUser.getFake(SHOW_BANNER_KEY); - showBanner.nextState(false); - - const sut = sutFactory(); - expect(await firstValueFrom(sut.showBanner$)).toBe(false); - expect(apiService.getShowUnassignedCiphersBanner).not.toHaveBeenCalled(); - }); - - it("fetches from server if local state has not been set yet", async () => { - apiService.getShowUnassignedCiphersBanner.mockResolvedValue(true); - - const showBanner = stateProvider.activeUser.getFake(SHOW_BANNER_KEY); - showBanner.nextState(undefined); - - const sut = sutFactory(); - - expect(await firstValueFrom(sut.showBanner$)).toBe(true); - expect(apiService.getShowUnassignedCiphersBanner).toHaveBeenCalledTimes(1); - }); -}); diff --git a/libs/angular/src/services/unassigned-items-banner.service.ts b/libs/angular/src/services/unassigned-items-banner.service.ts deleted file mode 100644 index db93d4c4fca..00000000000 --- a/libs/angular/src/services/unassigned-items-banner.service.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { Injectable } from "@angular/core"; -import { combineLatest, concatMap, map, startWith } from "rxjs"; - -import { - OrganizationService, - canAccessOrgAdmin, -} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; -import { - EnvironmentService, - Region, -} from "@bitwarden/common/platform/abstractions/environment.service"; -import { - StateProvider, - UNASSIGNED_ITEMS_BANNER_DISK, - UserKeyDefinition, -} from "@bitwarden/common/platform/state"; - -import { UnassignedItemsBannerApiService } from "./unassigned-items-banner.api.service"; - -export const SHOW_BANNER_KEY = new UserKeyDefinition( - UNASSIGNED_ITEMS_BANNER_DISK, - "showBanner", - { - deserializer: (b) => b, - clearOn: [], - }, -); - -/** Displays a banner that tells users how to move their unassigned items into a collection. */ -@Injectable({ providedIn: "root" }) -export class UnassignedItemsBannerService { - private _showBanner = this.stateProvider.getActive(SHOW_BANNER_KEY); - - showBanner$ = this._showBanner.state$.pipe( - concatMap(async (showBannerState) => { - // null indicates that the user has not seen or dismissed the banner yet - get the flag from server - if (showBannerState == null) { - const showBannerResponse = await this.apiService.getShowUnassignedCiphersBanner(); - await this._showBanner.update(() => showBannerResponse); - return showBannerResponse; - } - - return showBannerState; - }), - ); - - private adminConsoleOrg$ = this.organizationService.organizations$.pipe( - map((orgs) => orgs.find((o) => canAccessOrgAdmin(o))), - ); - - adminConsoleUrl$ = combineLatest([ - this.adminConsoleOrg$, - this.environmentService.environment$, - ]).pipe( - map(([org, environment]) => { - if (org == null || environment == null) { - return "#"; - } - - return environment.getWebVaultUrl() + "/#/organizations/" + org.id; - }), - ); - - bannerText$ = this.environmentService.environment$.pipe( - map((e) => - e?.getRegion() == Region.SelfHosted - ? "unassignedItemsBannerSelfHostNotice" - : "unassignedItemsBannerNotice", - ), - ); - - loading$ = combineLatest([this.adminConsoleUrl$, this.bannerText$]).pipe( - startWith(true), - map(() => false), - ); - - constructor( - private stateProvider: StateProvider, - private apiService: UnassignedItemsBannerApiService, - private environmentService: EnvironmentService, - private organizationService: OrganizationService, - ) {} - - async hideBanner() { - await this._showBanner.update(() => false); - } -} diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index 3f451e38b19..e9b154d2f4f 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -11,7 +11,6 @@ export enum FeatureFlag { ShowPaymentMethodWarningBanners = "show-payment-method-warning-banners", EnableConsolidatedBilling = "enable-consolidated-billing", AC1795_UpdatedSubscriptionStatusSection = "AC-1795_updated-subscription-status-section", - UnassignedItemsBanner = "unassigned-items-banner", EnableDeleteProvider = "AC-1218-delete-provider", ExtensionRefresh = "extension-refresh", RestrictProviderAccess = "restrict-provider-access", @@ -45,7 +44,6 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.ShowPaymentMethodWarningBanners]: FALSE, [FeatureFlag.EnableConsolidatedBilling]: FALSE, [FeatureFlag.AC1795_UpdatedSubscriptionStatusSection]: FALSE, - [FeatureFlag.UnassignedItemsBanner]: FALSE, [FeatureFlag.EnableDeleteProvider]: FALSE, [FeatureFlag.ExtensionRefresh]: FALSE, [FeatureFlag.RestrictProviderAccess]: FALSE, From 7588e06d2b76f10e02f7cf9794ac161ca110fa2e Mon Sep 17 00:00:00 2001 From: SmithThe4th Date: Fri, 12 Jul 2024 12:16:54 -0400 Subject: [PATCH 15/57] fix blank titles on menu options button (#10099) --- .../components/vault-items/vault-cipher-row.component.html | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.html b/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.html index 604dd4acadf..d4ce1e79ba5 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.html +++ b/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.html @@ -71,11 +71,10 @@ From 81a3dce774c3c8e05153a446370cfcb083552ddc Mon Sep 17 00:00:00 2001 From: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com> Date: Fri, 12 Jul 2024 12:20:16 -0500 Subject: [PATCH 16/57] [AC-2520] State migration/removal of Unassigned Items Banner state definition (#10080) * chore: create migration to remove banner dismissed data, refs AC-2520 * chore: remove unassigned items banner state definition, refs AC-2520 * fix: key like definition name updated to match original scope, refs AC-2520 --- .../src/platform/state/state-definitions.ts | 4 -- ...-unassigned-items-banner-dismissed.spec.ts | 50 +++++++++++++++++++ ...emove-unassigned-items-banner-dismissed.ts | 23 +++++++++ 3 files changed, 73 insertions(+), 4 deletions(-) create mode 100644 libs/common/src/state-migrations/migrations/67-remove-unassigned-items-banner-dismissed.spec.ts create mode 100644 libs/common/src/state-migrations/migrations/67-remove-unassigned-items-banner-dismissed.ts diff --git a/libs/common/src/platform/state/state-definitions.ts b/libs/common/src/platform/state/state-definitions.ts index 6cc2b181b64..53e9ca9fb6e 100644 --- a/libs/common/src/platform/state/state-definitions.ts +++ b/libs/common/src/platform/state/state-definitions.ts @@ -95,10 +95,6 @@ export const NEW_WEB_LAYOUT_BANNER_DISK = new StateDefinition("newWebLayoutBanne web: "disk-local", }); -export const UNASSIGNED_ITEMS_BANNER_DISK = new StateDefinition("unassignedItemsBanner", "disk", { - web: "disk-local", -}); - // Platform export const APPLICATION_ID_DISK = new StateDefinition("applicationId", "disk", { diff --git a/libs/common/src/state-migrations/migrations/67-remove-unassigned-items-banner-dismissed.spec.ts b/libs/common/src/state-migrations/migrations/67-remove-unassigned-items-banner-dismissed.spec.ts new file mode 100644 index 00000000000..2404920a433 --- /dev/null +++ b/libs/common/src/state-migrations/migrations/67-remove-unassigned-items-banner-dismissed.spec.ts @@ -0,0 +1,50 @@ +import { runMigrator } from "../migration-helper.spec"; +import { IRREVERSIBLE } from "../migrator"; + +import { RemoveUnassignedItemsBannerDismissed } from "./67-remove-unassigned-items-banner-dismissed"; + +describe("RemoveUnassignedItemsBannerDismissed", () => { + const sut = new RemoveUnassignedItemsBannerDismissed(66, 67); + + describe("migrate", () => { + it("deletes unassignedItemsBanner from all users", async () => { + const output = await runMigrator(sut, { + global_account_accounts: { + user1: { + email: "user1@email.com", + name: "User 1", + emailVerified: true, + }, + user2: { + email: "user2@email.com", + name: "User 2", + emailVerified: true, + }, + }, + user_user1_unassignedItemsBanner_showBanner: true, + user_user2_unassignedItemsBanner_showBanner: false, + }); + + expect(output).toEqual({ + global_account_accounts: { + user1: { + email: "user1@email.com", + name: "User 1", + emailVerified: true, + }, + user2: { + email: "user2@email.com", + name: "User 2", + emailVerified: true, + }, + }, + }); + }); + }); + + describe("rollback", () => { + it("is irreversible", async () => { + await expect(runMigrator(sut, {}, "rollback")).rejects.toThrow(IRREVERSIBLE); + }); + }); +}); diff --git a/libs/common/src/state-migrations/migrations/67-remove-unassigned-items-banner-dismissed.ts b/libs/common/src/state-migrations/migrations/67-remove-unassigned-items-banner-dismissed.ts new file mode 100644 index 00000000000..de3cee573df --- /dev/null +++ b/libs/common/src/state-migrations/migrations/67-remove-unassigned-items-banner-dismissed.ts @@ -0,0 +1,23 @@ +import { KeyDefinitionLike, MigrationHelper } from "../migration-helper"; +import { IRREVERSIBLE, Migrator } from "../migrator"; + +export const SHOW_BANNER: KeyDefinitionLike = { + key: "showBanner", + stateDefinition: { name: "unassignedItemsBanner" }, +}; + +export class RemoveUnassignedItemsBannerDismissed extends Migrator<66, 67> { + async migrate(helper: MigrationHelper): Promise { + await Promise.all( + (await helper.getAccounts()).map(async ({ userId }) => { + if (helper.getFromUser(userId, SHOW_BANNER) != null) { + await helper.removeFromUser(userId, SHOW_BANNER); + } + }), + ); + } + + async rollback(helper: MigrationHelper): Promise { + throw IRREVERSIBLE; + } +} From b7e102dd6db5eacbc8d1b3877b29ab3a71b02912 Mon Sep 17 00:00:00 2001 From: Jason Ng Date: Fri, 12 Jul 2024 16:19:22 -0400 Subject: [PATCH 17/57] PM-9632 added MP reprompt check to edit button in view-v2 (#10104) --- .../vault-v2/view-v2/view-v2.component.ts | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts index 4fe88da5550..91922162f96 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts @@ -102,7 +102,20 @@ export class ViewV2Component { return await cipher.decrypt(await this.cipherService.getKeyForCipherKeyDecryption(cipher)); } - editCipher() { + async checkForPasswordReprompt() { + this.passwordReprompted = + this.passwordReprompted || + (await this.passwordRepromptService.passwordRepromptCheck(this.cipher)); + if (!this.passwordReprompted) { + return false; + } + return true; + } + + async editCipher() { + if (!(await this.checkForPasswordReprompt())) { + return; + } if (this.cipher.isDeleted) { return false; } @@ -113,10 +126,7 @@ export class ViewV2Component { } delete = async (): Promise => { - this.passwordReprompted = - this.passwordReprompted || - (await this.passwordRepromptService.passwordRepromptCheck(this.cipher)); - if (!this.passwordReprompted) { + if (!(await this.checkForPasswordReprompt())) { return; } From eceec4b908c2de694567ed89a7a37235f7fdf323 Mon Sep 17 00:00:00 2001 From: Shane Melton Date: Fri, 12 Jul 2024 14:30:39 -0700 Subject: [PATCH 18/57] [PM-8526] Set secureNote type when creating a new note (#10107) --- .../src/cipher-form/components/cipher-form.component.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/libs/vault/src/cipher-form/components/cipher-form.component.ts b/libs/vault/src/cipher-form/components/cipher-form.component.ts index 9d5e0684d2d..00226b25ea6 100644 --- a/libs/vault/src/cipher-form/components/cipher-form.component.ts +++ b/libs/vault/src/cipher-form/components/cipher-form.component.ts @@ -16,7 +16,7 @@ import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { FormBuilder, ReactiveFormsModule } from "@angular/forms"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { CipherType } from "@bitwarden/common/vault/enums"; +import { CipherType, SecureNoteType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { AsyncActionsModule, @@ -185,6 +185,10 @@ export class CipherFormComponent implements AfterViewInit, OnInit, OnChanges, Ci this.updatedCipherView = Object.assign(this.updatedCipherView, this.originalCipherView); } else { this.updatedCipherView.type = this.config.cipherType; + + if (this.config.cipherType === CipherType.SecureNote) { + this.updatedCipherView.secureNote.type = SecureNoteType.Generic; + } } this.loading = false; From 06cf849b18c5ab30226eab5662f4ae16e480e4db Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Sat, 13 Jul 2024 00:48:59 +0200 Subject: [PATCH 19/57] [PM-9707] [Boostrap] Fix avatar and customize placement (#10093) The Avatar customize button appeared below the avatar after removing Boostrap. This resolves it by adding flex, it also has the added benefit of aligning the button vertically as it's currently slightly misaligned. --- apps/web/src/app/auth/settings/account/profile.component.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/web/src/app/auth/settings/account/profile.component.html b/apps/web/src/app/auth/settings/account/profile.component.html index 4464824c63e..93025420b26 100644 --- a/apps/web/src/app/auth/settings/account/profile.component.html +++ b/apps/web/src/app/auth/settings/account/profile.component.html @@ -19,8 +19,8 @@

-
- +
+ diff --git a/apps/browser/src/auth/popup/account-switching/services/account-switcher.service.ts b/apps/browser/src/auth/popup/account-switching/services/account-switcher.service.ts index d60b0dfaebc..7cc9f8a92f2 100644 --- a/apps/browser/src/auth/popup/account-switching/services/account-switcher.service.ts +++ b/apps/browser/src/auth/popup/account-switching/services/account-switcher.service.ts @@ -80,7 +80,7 @@ export class AccountSwitcherService { if (!hasMaxAccounts) { options.push({ - name: "Add account", + name: "addAccount", id: this.SPECIAL_ADD_ACCOUNT_ID, isActive: false, }); From fb311bfb4b9beca787faa884434a3410513fe733 Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Mon, 15 Jul 2024 03:26:11 -0700 Subject: [PATCH 22/57] [PM-4964] migrate exposed passwords report components (#9952) * WIP - migrate exposed passwords report components * lint fix * migrate components in reports * migrate breach and unsecured websites reports * undo change routing * revert changes to reports * revert changes * fix icon and padding in exposed passwords report * fix icon and padding in exposed passwords report * fix exposed passwords report cipher state * [PM-4965] Migrate inactive two factor report (#10043) * migrate inactive two factor report * fix icon and padding in inactive two factor report * [PM-4967] Migrate reused passwords report components (#10044) * migrate components in reused passwords report * fix icon and padding in reused passwords report * [PM-4969] Migrate weak passwords report components (#10048) * migrate weak passwords report component * migrate weak passwords report component * Revert "migrate weak passwords report component" This reverts commit 0e453aafc154fee48f37bb22a7f24cdf2847a43c. * Revert "migrate weak passwords report component" This reverts commit e8e9b01997ce453e9292b7f49ccc8ac81bab690d. * Revert "Revert "migrate weak passwords report component"" This reverts commit 8cd2421cb0107cf579f3a83acb97f9f3dbf5e5e6. * fix padding and icon size in weak passwords report --------- Co-authored-by: jordan-bite --- .../reports/pages/cipher-report.component.ts | 5 + .../exposed-passwords-report.component.html | 49 ++++--- .../exposed-passwords-report.component.ts | 1 - .../inactive-two-factor-report.component.html | 127 +++++++++--------- .../reused-passwords-report.component.html | 49 ++++--- .../weak-passwords-report.component.html | 49 ++++--- 6 files changed, 139 insertions(+), 141 deletions(-) diff --git a/apps/web/src/app/tools/reports/pages/cipher-report.component.ts b/apps/web/src/app/tools/reports/pages/cipher-report.component.ts index 4e63dd5cc96..b6a34cf2518 100644 --- a/apps/web/src/app/tools/reports/pages/cipher-report.component.ts +++ b/apps/web/src/app/tools/reports/pages/cipher-report.component.ts @@ -9,6 +9,7 @@ import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.servi import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { TableDataSource } from "@bitwarden/components"; import { PasswordRepromptService } from "@bitwarden/vault"; import { AddEditComponent } from "../../../vault/individual-vault/add-edit.component"; @@ -24,6 +25,7 @@ export class CipherReportComponent implements OnDestroy { hasLoaded = false; ciphers: CipherView[] = []; allCiphers: CipherView[] = []; + dataSource = new TableDataSource(); organization: Organization; organizations: Organization[]; organizations$: Observable; @@ -104,6 +106,7 @@ export class CipherReportComponent implements OnDestroy { } else { this.ciphers = this.ciphers.filter((c: any) => c.orgFilterStatus === status); } + this.dataSource.data = this.ciphers; } async load() { @@ -193,6 +196,8 @@ export class CipherReportComponent implements OnDestroy { return ciph; }); + this.dataSource.data = this.ciphers; + if (this.filterStatus.length > 2) { this.showFilterToggle = true; this.vaultMsg = "vaults"; diff --git a/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.html b/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.html index 30801a42fdc..5d2c87bdc47 100644 --- a/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.html +++ b/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.html @@ -6,13 +6,13 @@ {{ "checkExposedPasswords" | i18n }}
- + {{ "noExposedPasswords" | i18n }} - + - + {{ "exposedPasswordsFoundReportDesc" | i18n: (ciphers.length | number) : vaultMsg }} - + - - + + - - - + + + - - -
{{ "name" | i18n }} {{ "owner" | i18n }}
- + - + + {{ c.name }}{{ r.name }} - {{ c.name }} + {{ r.name }} - + {{ "shared" | i18n }} - + {{ "attachments" | i18n }}
- {{ c.subTitle }} + {{ r.subTitle }}
- {{ "exposedXTimes" | i18n: (exposedPasswordMap.get(c.id) | number) }} + {{ "exposedXTimes" | i18n: (exposedPasswordMap.get(r.id) | number) }}
+ +
diff --git a/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.ts b/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.ts index cabc7bdfa12..8503174a937 100644 --- a/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.ts +++ b/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.ts @@ -11,7 +11,6 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { PasswordRepromptService } from "@bitwarden/vault"; import { CipherReportComponent } from "./cipher-report.component"; - @Component({ selector: "app-exposed-passwords-report", templateUrl: "exposed-passwords-report.component.html", diff --git a/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.html b/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.html index ae03a3bcb80..a757f0edc5e 100644 --- a/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.html +++ b/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.html @@ -11,13 +11,13 @@ {{ "loading" | i18n }}
- + {{ "noInactive2fa" | i18n }} - + - + {{ "inactive2faFoundReportDesc" | i18n: (ciphers.length | number) : vaultMsg }} - + - - + + - + - - - + + + - - - - -
{{ "name" | i18n }} {{ "owner" | i18n }}
- - - {{ - c.name - }} - - +
+ + + {{ r.name }} + + + {{ "shared" | i18n }} + + + + {{ "attachments" | i18n }} + +
+ {{ r.subTitle }} +
+ - - - - - {{ "instructions" | i18n }} -
+ > + + + + + {{ "instructions" | i18n }} + + + +
diff --git a/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.html b/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.html index 549773ba8ce..06176cdcd5a 100644 --- a/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.html +++ b/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.html @@ -11,13 +11,13 @@ {{ "loading" | i18n }}
- + {{ "noReusedPasswords" | i18n }} - + - + {{ "reusedPasswordsFoundReportDesc" | i18n: (ciphers.length | number) : vaultMsg }} - + - - + + - - - + + + - - -
{{ "name" | i18n }} {{ "owner" | i18n }}
- + - + + {{ c.name }}{{ r.name }} - {{ c.name }} + {{ r.name }} - + {{ "shared" | i18n }} - + {{ "attachments" | i18n }}
- {{ c.subTitle }} + {{ r.subTitle }}
- {{ "reusedXTimes" | i18n: passwordUseMap.get(c.login.password) }} + {{ "reusedXTimes" | i18n: passwordUseMap.get(r.login.password) }}
+ +
diff --git a/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.html b/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.html index a943c8c29ec..36385dd087f 100644 --- a/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.html +++ b/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.html @@ -11,13 +11,13 @@ {{ "loading" | i18n }}
- + {{ "noWeakPasswords" | i18n }} - + - + {{ "weakPasswordsFoundReportDesc" | i18n: (ciphers.length | number) : vaultMsg }} - + - - + + - - - + + + - -
{{ "name" | i18n }} {{ "owner" | i18n }}
- + - + {{ c.name }}{{ r.name }} - {{ c.name }} + {{ r.name }} - + {{ "shared" | i18n }} - + {{ "attachments" | i18n }}
- {{ c.subTitle }} + {{ r.subTitle }}
- - {{ passwordStrengthMap.get(c.id)[0] | i18n }} + + {{ passwordStrengthMap.get(r.id)[0] | i18n }}
+ +
From 0a04a47c753605b15ffe8e0ca3d16b5f3df2bc96 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 15 Jul 2024 12:52:31 +0200 Subject: [PATCH 23/57] Autosync the updated translations (#10117) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/browser/src/_locales/ar/messages.json | 17 ++----- apps/browser/src/_locales/az/messages.json | 23 +++------ apps/browser/src/_locales/be/messages.json | 17 ++----- apps/browser/src/_locales/bg/messages.json | 17 ++----- apps/browser/src/_locales/bn/messages.json | 17 ++----- apps/browser/src/_locales/bs/messages.json | 17 ++----- apps/browser/src/_locales/ca/messages.json | 17 ++----- apps/browser/src/_locales/cs/messages.json | 17 ++----- apps/browser/src/_locales/cy/messages.json | 17 ++----- apps/browser/src/_locales/da/messages.json | 17 ++----- apps/browser/src/_locales/de/messages.json | 17 ++----- apps/browser/src/_locales/el/messages.json | 17 ++----- apps/browser/src/_locales/en_GB/messages.json | 17 ++----- apps/browser/src/_locales/en_IN/messages.json | 17 ++----- apps/browser/src/_locales/es/messages.json | 17 ++----- apps/browser/src/_locales/et/messages.json | 17 ++----- apps/browser/src/_locales/eu/messages.json | 17 ++----- apps/browser/src/_locales/fa/messages.json | 17 ++----- apps/browser/src/_locales/fi/messages.json | 49 +++++++------------ apps/browser/src/_locales/fil/messages.json | 17 ++----- apps/browser/src/_locales/fr/messages.json | 17 ++----- apps/browser/src/_locales/gl/messages.json | 17 ++----- apps/browser/src/_locales/he/messages.json | 17 ++----- apps/browser/src/_locales/hi/messages.json | 17 ++----- apps/browser/src/_locales/hr/messages.json | 17 ++----- apps/browser/src/_locales/hu/messages.json | 17 ++----- apps/browser/src/_locales/id/messages.json | 17 ++----- apps/browser/src/_locales/it/messages.json | 17 ++----- apps/browser/src/_locales/ja/messages.json | 17 ++----- apps/browser/src/_locales/ka/messages.json | 17 ++----- apps/browser/src/_locales/km/messages.json | 17 ++----- apps/browser/src/_locales/kn/messages.json | 17 ++----- apps/browser/src/_locales/ko/messages.json | 17 ++----- apps/browser/src/_locales/lt/messages.json | 17 ++----- apps/browser/src/_locales/lv/messages.json | 17 ++----- apps/browser/src/_locales/ml/messages.json | 17 ++----- apps/browser/src/_locales/mr/messages.json | 17 ++----- apps/browser/src/_locales/my/messages.json | 17 ++----- apps/browser/src/_locales/nb/messages.json | 17 ++----- apps/browser/src/_locales/ne/messages.json | 17 ++----- apps/browser/src/_locales/nl/messages.json | 17 ++----- apps/browser/src/_locales/nn/messages.json | 17 ++----- apps/browser/src/_locales/or/messages.json | 17 ++----- apps/browser/src/_locales/pl/messages.json | 17 ++----- apps/browser/src/_locales/pt_BR/messages.json | 17 ++----- apps/browser/src/_locales/pt_PT/messages.json | 27 +++------- apps/browser/src/_locales/ro/messages.json | 17 ++----- apps/browser/src/_locales/ru/messages.json | 17 ++----- apps/browser/src/_locales/si/messages.json | 17 ++----- apps/browser/src/_locales/sk/messages.json | 17 ++----- apps/browser/src/_locales/sl/messages.json | 17 ++----- apps/browser/src/_locales/sr/messages.json | 39 ++++++--------- apps/browser/src/_locales/sv/messages.json | 17 ++----- apps/browser/src/_locales/te/messages.json | 17 ++----- apps/browser/src/_locales/th/messages.json | 17 ++----- apps/browser/src/_locales/tr/messages.json | 17 ++----- apps/browser/src/_locales/uk/messages.json | 17 ++----- apps/browser/src/_locales/vi/messages.json | 17 ++----- apps/browser/src/_locales/zh_CN/messages.json | 17 ++----- apps/browser/src/_locales/zh_TW/messages.json | 17 ++----- 60 files changed, 215 insertions(+), 875 deletions(-) diff --git a/apps/browser/src/_locales/ar/messages.json b/apps/browser/src/_locales/ar/messages.json index 197cf3eb920..4eb6c139792 100644 --- a/apps/browser/src/_locales/ar/messages.json +++ b/apps/browser/src/_locales/ar/messages.json @@ -3358,20 +3358,6 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Auto-fill suggestions" }, @@ -3630,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/az/messages.json b/apps/browser/src/_locales/az/messages.json index 4c82d3726a3..72490926acf 100644 --- a/apps/browser/src/_locales/az/messages.json +++ b/apps/browser/src/_locales/az/messages.json @@ -3358,20 +3358,6 @@ "passkeyRemoved": { "message": "Parol silindi" }, - "unassignedItemsBannerNotice": { - "message": "Bildiriş: Təyin edilməyən təşkilat elementləri artıq Bütün Anbarlar görünüşündə görünən olmayacaq və yalnız Admin Konsolu vasitəsilə əlçatan olacaq." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Bildiriş: 16 May 2024-cü il tarixindən etibarən, təyin edilməyən təşkilat elementləri Bütün Anbarlar görünüşündə görünən olmayacaq və yalnız Admin Konsolu vasitəsilə əlçatan olacaq." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Bu elementləri görünən etmək üçün", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "bir kolleksiyaya təyin edin.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Avto-doldurma təklifləri" }, @@ -3602,13 +3588,13 @@ "message": "Filtrlər" }, "personalDetails": { - "message": "Personal details" + "message": "Şəxsi detallar" }, "identification": { - "message": "Identification" + "message": "İdentifikasiya" }, "contactInfo": { - "message": "Contact info" + "message": "Əlaqə məlumatı" }, "downloadAttachment": { "message": "$ITEMNAME$ - endir", @@ -3630,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/be/messages.json b/apps/browser/src/_locales/be/messages.json index f772077008f..b4fdffc479d 100644 --- a/apps/browser/src/_locales/be/messages.json +++ b/apps/browser/src/_locales/be/messages.json @@ -3358,20 +3358,6 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Auto-fill suggestions" }, @@ -3630,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/bg/messages.json b/apps/browser/src/_locales/bg/messages.json index 74a0a5b8cad..db1c266177b 100644 --- a/apps/browser/src/_locales/bg/messages.json +++ b/apps/browser/src/_locales/bg/messages.json @@ -3358,20 +3358,6 @@ "passkeyRemoved": { "message": "Секретният ключ е премахнат" }, - "unassignedItemsBannerNotice": { - "message": "Известие: неразпределените елементи в организацията вече няма да се виждат в изгледа с всички трезори, а са достъпни само през Административната конзола." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Известие: след 16 май 2024, неразпределените елементи в организацията вече няма да се виждат в изгледа с всички трезори, а ще бъдат достъпни само през Административната конзола." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Добавете тези елементи към колекция в", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "за да ги направите видими.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Автоматично попълване на предложения" }, @@ -3630,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/bn/messages.json b/apps/browser/src/_locales/bn/messages.json index 9b4d69b08dd..528248476d0 100644 --- a/apps/browser/src/_locales/bn/messages.json +++ b/apps/browser/src/_locales/bn/messages.json @@ -3358,20 +3358,6 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Auto-fill suggestions" }, @@ -3630,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/bs/messages.json b/apps/browser/src/_locales/bs/messages.json index 7d4fc507412..0040eb2a433 100644 --- a/apps/browser/src/_locales/bs/messages.json +++ b/apps/browser/src/_locales/bs/messages.json @@ -3358,20 +3358,6 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Auto-fill suggestions" }, @@ -3630,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/ca/messages.json b/apps/browser/src/_locales/ca/messages.json index 13cd7d0b9ce..467673e6c91 100644 --- a/apps/browser/src/_locales/ca/messages.json +++ b/apps/browser/src/_locales/ca/messages.json @@ -3358,20 +3358,6 @@ "passkeyRemoved": { "message": "Clau de pas suprimida" }, - "unassignedItemsBannerNotice": { - "message": "Avís: els elements de l'organització no assignats ja no són visibles a la visualització de Totes les caixes fortes i només es poden accedir des de la Consola d'administració." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Avís: el 16 de maig de 2024, els elements de l'organització no assignats deixaran de ser visibles a la visualització de Totes les caixes fortes i només es podran accedir des de la Consola d'administració." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assigna aquests elements a una col·lecció de", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "per fer-los visibles.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Auto-fill suggestions" }, @@ -3630,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/cs/messages.json b/apps/browser/src/_locales/cs/messages.json index 76232e6c8fa..1067219e023 100644 --- a/apps/browser/src/_locales/cs/messages.json +++ b/apps/browser/src/_locales/cs/messages.json @@ -3358,20 +3358,6 @@ "passkeyRemoved": { "message": "Přístupový klíč byl odebrán" }, - "unassignedItemsBannerNotice": { - "message": "Upozornění: Nepřiřazené položky organizace již nejsou viditelné ve vašem zobrazení všech trezorů a jsou nyní přístupné pouze v konzoli správce." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Upozornění: 16. květba 2024 již nebudou nepřiřazené položky organizace viditelné ve vašem zobrazení všech trezorů a budou přístupné pouze v konzoli správce." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Přiřadit tyto položky ke kolekci z", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "aby byly viditelné.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Návrhy automatického vyplňování" }, @@ -3630,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/cy/messages.json b/apps/browser/src/_locales/cy/messages.json index 1cc262b4dc5..55c7b494e16 100644 --- a/apps/browser/src/_locales/cy/messages.json +++ b/apps/browser/src/_locales/cy/messages.json @@ -3358,20 +3358,6 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Auto-fill suggestions" }, @@ -3630,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/da/messages.json b/apps/browser/src/_locales/da/messages.json index 6acfde47394..5ff1553f895 100644 --- a/apps/browser/src/_locales/da/messages.json +++ b/apps/browser/src/_locales/da/messages.json @@ -3358,20 +3358,6 @@ "passkeyRemoved": { "message": "Adgangsnøgle fjernet" }, - "unassignedItemsBannerNotice": { - "message": "Bemærk: Utildelte organisationsemner er ikke længere synlige i Alle Bokse-visningen, men er kun tilgængelige via Admin-konsol." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Bemærk: Pr. 16. maj 2024 er utildelte organisationsemner er ikke længere synlige i Alle Bokse-visningen, men er kun tilgængelige via Admin-konsol." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Tildel disse emner til en samling via", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "for at gøre dem synlige.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Autoudfyldningsforslag" }, @@ -3630,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/de/messages.json b/apps/browser/src/_locales/de/messages.json index 446e13c1139..444b2df0977 100644 --- a/apps/browser/src/_locales/de/messages.json +++ b/apps/browser/src/_locales/de/messages.json @@ -3358,20 +3358,6 @@ "passkeyRemoved": { "message": "Passkey entfernt" }, - "unassignedItemsBannerNotice": { - "message": "Hinweis: Nicht zugeordnete Organisationseinträge sind nicht mehr in der Ansicht aller Tresore sichtbar und nur über die Administrator-Konsole zugänglich." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Hinweis: Ab dem 16. Mai 2024 sind nicht zugewiesene Organisationselemente nicht mehr in der Ansicht aller Tresore sichtbar und nur über die Administrator-Konsole zugänglich." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Weise diese Einträge einer Sammlung aus der", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "zu, um sie sichtbar zu machen.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Vorschläge zum Auto-Ausfüllen" }, @@ -3630,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Konto hinzufügen" } } diff --git a/apps/browser/src/_locales/el/messages.json b/apps/browser/src/_locales/el/messages.json index 9bfb8816f87..e60f2b61d16 100644 --- a/apps/browser/src/_locales/el/messages.json +++ b/apps/browser/src/_locales/el/messages.json @@ -3358,20 +3358,6 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Auto-fill suggestions" }, @@ -3630,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/en_GB/messages.json b/apps/browser/src/_locales/en_GB/messages.json index 575959fee9b..886c6082673 100644 --- a/apps/browser/src/_locales/en_GB/messages.json +++ b/apps/browser/src/_locales/en_GB/messages.json @@ -3358,20 +3358,6 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organisation items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organisation items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Auto-fill suggestions" }, @@ -3630,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/en_IN/messages.json b/apps/browser/src/_locales/en_IN/messages.json index 3621dab7aa0..6db451bdb34 100644 --- a/apps/browser/src/_locales/en_IN/messages.json +++ b/apps/browser/src/_locales/en_IN/messages.json @@ -3358,20 +3358,6 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organisation items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organisation items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Auto-fill suggestions" }, @@ -3630,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/es/messages.json b/apps/browser/src/_locales/es/messages.json index d70e3fbe4b0..cad044f9921 100644 --- a/apps/browser/src/_locales/es/messages.json +++ b/apps/browser/src/_locales/es/messages.json @@ -3358,20 +3358,6 @@ "passkeyRemoved": { "message": "Clave de acceso eliminada" }, - "unassignedItemsBannerNotice": { - "message": "Aviso: Los elementos de organización no asignados ya no son visibles en la vista de Todas las cajas fuertes y solo son accesibles a través de la Consola de Administrador." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Aviso: El 16 de mayo de 2024, los elementos de organización no asignados no serán visibles en la vista de Todas las cajas fuertes y solo serán accesibles a través de la Consola de Administrador." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Asignar estos elementos a una colección de", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "para hcerlos visibles.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Autocompletar sugerencias" }, @@ -3630,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Añadir cuenta" } } diff --git a/apps/browser/src/_locales/et/messages.json b/apps/browser/src/_locales/et/messages.json index 7dc7380dcd9..d6b7120294f 100644 --- a/apps/browser/src/_locales/et/messages.json +++ b/apps/browser/src/_locales/et/messages.json @@ -3358,20 +3358,6 @@ "passkeyRemoved": { "message": "Pääsuvõti on eemaldatud" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Auto-fill suggestions" }, @@ -3630,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/eu/messages.json b/apps/browser/src/_locales/eu/messages.json index 0b6d301d031..414d1764039 100644 --- a/apps/browser/src/_locales/eu/messages.json +++ b/apps/browser/src/_locales/eu/messages.json @@ -3358,20 +3358,6 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Auto-fill suggestions" }, @@ -3630,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/fa/messages.json b/apps/browser/src/_locales/fa/messages.json index a605b94dcbb..ffc4370f20d 100644 --- a/apps/browser/src/_locales/fa/messages.json +++ b/apps/browser/src/_locales/fa/messages.json @@ -3358,20 +3358,6 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Auto-fill suggestions" }, @@ -3630,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/fi/messages.json b/apps/browser/src/_locales/fi/messages.json index 15d5d85c1a1..4758613a2ea 100644 --- a/apps/browser/src/_locales/fi/messages.json +++ b/apps/browser/src/_locales/fi/messages.json @@ -557,16 +557,16 @@ "message": "Suojaus" }, "confirmMasterPassword": { - "message": "Confirm master password" + "message": "Vahvista pääsalasana" }, "masterPassword": { - "message": "Master password" + "message": "Pääsalasana" }, "masterPassImportant": { - "message": "Your master password cannot be recovered if you forget it!" + "message": "Pääsalasanasi palauttaminen ei ole mahdollista, jos unohdat sen!" }, "masterPassHintLabel": { - "message": "Master password hint" + "message": "Pääsalasanan vihje" }, "errorOccurred": { "message": "Tapahtui virhe" @@ -1484,7 +1484,7 @@ } }, "viewItemHeader": { - "message": "View $TYPE$", + "message": "Näytä $TYPE$", "placeholders": { "type": { "content": "$1", @@ -2186,7 +2186,7 @@ "message": "Sähköpostiosoite on vahvistettava" }, "emailVerifiedV2": { - "message": "Email verified" + "message": "Sähköpostiosoite on vahvistettu" }, "emailVerificationRequiredDesc": { "message": "Sinun on vahvistettava sähköpostiosoitteesi käyttääksesi ominaisuutta. Voit vahvistaa osoitteesi verkkoholvissa." @@ -3358,20 +3358,6 @@ "passkeyRemoved": { "message": "Suojausavain poistettiin" }, - "unassignedItemsBannerNotice": { - "message": "Huomioi: Määrittämättömät organisaatiokohteet eivät enää näy \"Kaikki holvit\" -näkymässä, vaan ne ovat käytettävissä vain Hallintapaneelista." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Huomioi: Alkaen 16. toukokuuta 2024, määrittämättömät organisaatiokohteet eivät enää näy \"Kaikki holvit\" -näkymässä, vaan ne ovat käytettävissä vain Hallintapaneelista." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Määritä nämä kohteet kokoelmaan", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": ", jotta ne näkyvät.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Automaattitäytä ehdotukset" }, @@ -3545,22 +3531,22 @@ "message": "Käytöstä poistettujen organisaatioiden kohteet eivät ole käytettävissä. Ole yhteydessä organisaation omistajaan saadaksesi apua." }, "additionalInformation": { - "message": "Additional information" + "message": "Lisätietoja" }, "itemHistory": { - "message": "Item history" + "message": "Kohdehistoria" }, "lastEdited": { - "message": "Last edited" + "message": "Viimeksi muokattu" }, "ownerYou": { - "message": "Owner: You" + "message": "Omistaja: Sinä" }, "linked": { - "message": "Linked" + "message": "Linkitetty" }, "copySuccessful": { - "message": "Copy Successful" + "message": "Kopiointi onnistui" }, "upload": { "message": "Lähetä" @@ -3602,16 +3588,16 @@ "message": "Suodattimet" }, "personalDetails": { - "message": "Personal details" + "message": "Henkilökohtaiset tiedot" }, "identification": { - "message": "Identification" + "message": "Tunnistautuminen" }, "contactInfo": { - "message": "Contact info" + "message": "Yhteystiedot" }, "downloadAttachment": { - "message": "Download - $ITEMNAME$", + "message": "Lataa - $ITEMNAME$", "placeholders": { "itemname": { "content": "$1", @@ -3630,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/fil/messages.json b/apps/browser/src/_locales/fil/messages.json index 4a86591c30d..5efa5ca5cf3 100644 --- a/apps/browser/src/_locales/fil/messages.json +++ b/apps/browser/src/_locales/fil/messages.json @@ -3358,20 +3358,6 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Auto-fill suggestions" }, @@ -3630,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/fr/messages.json b/apps/browser/src/_locales/fr/messages.json index 52908f8a8c5..d0f6aa710cb 100644 --- a/apps/browser/src/_locales/fr/messages.json +++ b/apps/browser/src/_locales/fr/messages.json @@ -3358,20 +3358,6 @@ "passkeyRemoved": { "message": "Clé d'identification (passkey) retirée" }, - "unassignedItemsBannerNotice": { - "message": "Remarque : Les éléments d'organisation non assignés ne sont plus visibles dans la vue Tous les coffres et ne sont maintenant accessibles que via la Console Admin." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Remarque : À partir du 16 mai 2024, les éléments d'organisation non assignés ne seront plus visibles dans votre vue Tous les coffres sur les appareils et ne seront maintenant accessibles que via la Console Admin." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Ajouter ces éléments à une collection depuis la", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "pour les rendre visibles.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Suggestions de saisie automatique" }, @@ -3630,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/gl/messages.json b/apps/browser/src/_locales/gl/messages.json index ce9949704f5..c9b15d4ea0a 100644 --- a/apps/browser/src/_locales/gl/messages.json +++ b/apps/browser/src/_locales/gl/messages.json @@ -3358,20 +3358,6 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Aviso: O 16 de maio de 2024, os elementos de organización non asignados non serán visíbeis na vista de Todas as caixas fortes e só serán accesíbeis a través da Consola de Administrador." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Auto-fill suggestions" }, @@ -3630,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/he/messages.json b/apps/browser/src/_locales/he/messages.json index 0de12256696..31d20be247e 100644 --- a/apps/browser/src/_locales/he/messages.json +++ b/apps/browser/src/_locales/he/messages.json @@ -3358,20 +3358,6 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Auto-fill suggestions" }, @@ -3630,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/hi/messages.json b/apps/browser/src/_locales/hi/messages.json index 3fa5152ac7b..4b23d203704 100644 --- a/apps/browser/src/_locales/hi/messages.json +++ b/apps/browser/src/_locales/hi/messages.json @@ -3358,20 +3358,6 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Auto-fill suggestions" }, @@ -3630,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/hr/messages.json b/apps/browser/src/_locales/hr/messages.json index 4514d1b3cd5..266a7b2d323 100644 --- a/apps/browser/src/_locales/hr/messages.json +++ b/apps/browser/src/_locales/hr/messages.json @@ -3358,20 +3358,6 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Auto-fill suggestions" }, @@ -3630,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/hu/messages.json b/apps/browser/src/_locales/hu/messages.json index 04bd7435f6b..598f122b4f8 100644 --- a/apps/browser/src/_locales/hu/messages.json +++ b/apps/browser/src/_locales/hu/messages.json @@ -3358,20 +3358,6 @@ "passkeyRemoved": { "message": "A jelszó eltávolításra került." }, - "unassignedItemsBannerNotice": { - "message": "Megjegyzés: A nem hozzárendelt szervezeti elemek már nem láthatók az Összes széf nézetben a különböző eszközökön és csak az Adminisztrátori konzolon keresztül érhetők el." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Megjegyzés: 2024. május 16-tól a nem hozzárendelt szervezeti elemek többé nem lesznek láthatók az Összes széf nézetben a különböző eszközökön és csak az Adminisztrátori konzolon keresztül lesznek elérhetők." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Rendeljük hozzá ezeket az elemeket a gyűjteményhez", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "a láthatósághoz.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Automatikus kitöltés javaslatok" }, @@ -3630,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/id/messages.json b/apps/browser/src/_locales/id/messages.json index f07aebf9c5f..8625ce67000 100644 --- a/apps/browser/src/_locales/id/messages.json +++ b/apps/browser/src/_locales/id/messages.json @@ -3358,20 +3358,6 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Auto-fill suggestions" }, @@ -3630,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/it/messages.json b/apps/browser/src/_locales/it/messages.json index 5d517cffaf4..b4f3e1d240d 100644 --- a/apps/browser/src/_locales/it/messages.json +++ b/apps/browser/src/_locales/it/messages.json @@ -3358,20 +3358,6 @@ "passkeyRemoved": { "message": "Passkey rimossa" }, - "unassignedItemsBannerNotice": { - "message": "Avviso: gli elementi dell'organizzazione non assegnati non sono più visibili nella visualizzazione Tutte le Cassaforti su tutti i dispositivi e sono ora accessibili solo tramite la Console di amministrazione." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Avviso: dal 16 maggio 2024, gli elementi dell'organizzazione non assegnati non saranno più visibili nella visualizzazione Tutte le Cassaforti su tutti i dispositivi e saranno accessibili solo tramite la Console di amministrazione." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assegna questi elementi ad una raccolta dalla", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "per renderli visibili.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Suggerimenti per il riempimento automatico" }, @@ -3630,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/ja/messages.json b/apps/browser/src/_locales/ja/messages.json index 6e001615899..ad45122d57b 100644 --- a/apps/browser/src/_locales/ja/messages.json +++ b/apps/browser/src/_locales/ja/messages.json @@ -3358,20 +3358,6 @@ "passkeyRemoved": { "message": "パスキーを削除しました" }, - "unassignedItemsBannerNotice": { - "message": "注意: 割り当てられていない組織アイテムは、すべての保管庫ビューでは表示されなくなり、管理コンソールからのみアクセスできるようになります。" - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "お知らせ:2024年5月16日に、 割り当てられていない組織アイテムは、すべての保管庫ビューに表示されなくなり、管理コンソールからのみアクセス可能になります。" - }, - "unassignedItemsBannerCTAPartOne": { - "message": "これらのアイテムのコレクションへの割り当てを", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "で実行すると表示できるようになります。", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "候補を自動入力する" }, @@ -3630,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/ka/messages.json b/apps/browser/src/_locales/ka/messages.json index 281de94b0d4..df8dc0cce3e 100644 --- a/apps/browser/src/_locales/ka/messages.json +++ b/apps/browser/src/_locales/ka/messages.json @@ -3358,20 +3358,6 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Auto-fill suggestions" }, @@ -3630,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/km/messages.json b/apps/browser/src/_locales/km/messages.json index f66a8d3c08a..fa76cf7060a 100644 --- a/apps/browser/src/_locales/km/messages.json +++ b/apps/browser/src/_locales/km/messages.json @@ -3358,20 +3358,6 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Auto-fill suggestions" }, @@ -3630,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/kn/messages.json b/apps/browser/src/_locales/kn/messages.json index 8f41e8fe20c..fc2b2711c9f 100644 --- a/apps/browser/src/_locales/kn/messages.json +++ b/apps/browser/src/_locales/kn/messages.json @@ -3358,20 +3358,6 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Auto-fill suggestions" }, @@ -3630,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/ko/messages.json b/apps/browser/src/_locales/ko/messages.json index 6da6f8d5e70..9762761b366 100644 --- a/apps/browser/src/_locales/ko/messages.json +++ b/apps/browser/src/_locales/ko/messages.json @@ -3358,20 +3358,6 @@ "passkeyRemoved": { "message": "패스키 제거됨" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Auto-fill suggestions" }, @@ -3630,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/lt/messages.json b/apps/browser/src/_locales/lt/messages.json index 7e9cf75db60..f09e9c21caa 100644 --- a/apps/browser/src/_locales/lt/messages.json +++ b/apps/browser/src/_locales/lt/messages.json @@ -3358,20 +3358,6 @@ "passkeyRemoved": { "message": "Pašalintas slaptaraktis" }, - "unassignedItemsBannerNotice": { - "message": "Pranešimas: nepriskirti organizacijos elementai nebėra matomi peržiūros rodinyje Visi saugyklos ir yra pasiekiami tik per Administratoriaus konsolę." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Pranešimas: 2024 m. gegužės 16 d. nepriskirti organizacijos elementai nebėra matomi peržiūros rodinyje Visi saugyklos ir yra pasiekiami tik per Administratoriaus konsolę." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Priskirkite šiuos elementus kolekcijai iš", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": ", kad jie būtų matomi.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Auto-fill suggestions" }, @@ -3630,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/lv/messages.json b/apps/browser/src/_locales/lv/messages.json index 23d33bf79d7..02688c4943f 100644 --- a/apps/browser/src/_locales/lv/messages.json +++ b/apps/browser/src/_locales/lv/messages.json @@ -3358,20 +3358,6 @@ "passkeyRemoved": { "message": "Piekļuves atslēga noņemta" }, - "unassignedItemsBannerNotice": { - "message": "Jāņem vērā: nepiešķirti apvienības vienumi vairs nav redzami skatā \"Visas glabātavas\" un ir pieejami tikai pārvaldības konsolē." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Jāņem vērā: no 2024. gada 16. maija nepiešķirti apvienības vienumi vairs nebūs redzami skatā \"Visas glabātavas\" un būs pieejami tikai pārvaldības konsolē." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Piešķirt šos vienumus krājumam", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "lai padarītu tos redzamus.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Ieteikumi automātiskajai aizpildei" }, @@ -3630,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/ml/messages.json b/apps/browser/src/_locales/ml/messages.json index 2f13c4a25d8..7d53653e2c8 100644 --- a/apps/browser/src/_locales/ml/messages.json +++ b/apps/browser/src/_locales/ml/messages.json @@ -3358,20 +3358,6 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Auto-fill suggestions" }, @@ -3630,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/mr/messages.json b/apps/browser/src/_locales/mr/messages.json index 2b6091519cf..3cf67b119b2 100644 --- a/apps/browser/src/_locales/mr/messages.json +++ b/apps/browser/src/_locales/mr/messages.json @@ -3358,20 +3358,6 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Auto-fill suggestions" }, @@ -3630,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/my/messages.json b/apps/browser/src/_locales/my/messages.json index f66a8d3c08a..fa76cf7060a 100644 --- a/apps/browser/src/_locales/my/messages.json +++ b/apps/browser/src/_locales/my/messages.json @@ -3358,20 +3358,6 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Auto-fill suggestions" }, @@ -3630,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/nb/messages.json b/apps/browser/src/_locales/nb/messages.json index c6876dc2e28..33c362d5b87 100644 --- a/apps/browser/src/_locales/nb/messages.json +++ b/apps/browser/src/_locales/nb/messages.json @@ -3358,20 +3358,6 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Auto-fill suggestions" }, @@ -3630,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/ne/messages.json b/apps/browser/src/_locales/ne/messages.json index f66a8d3c08a..fa76cf7060a 100644 --- a/apps/browser/src/_locales/ne/messages.json +++ b/apps/browser/src/_locales/ne/messages.json @@ -3358,20 +3358,6 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Auto-fill suggestions" }, @@ -3630,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/nl/messages.json b/apps/browser/src/_locales/nl/messages.json index 3f563d75fa3..b3d04e67c75 100644 --- a/apps/browser/src/_locales/nl/messages.json +++ b/apps/browser/src/_locales/nl/messages.json @@ -3358,20 +3358,6 @@ "passkeyRemoved": { "message": "Passkey verwijderd" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Suggesties voor automatisch invullen" }, @@ -3630,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/nn/messages.json b/apps/browser/src/_locales/nn/messages.json index f66a8d3c08a..fa76cf7060a 100644 --- a/apps/browser/src/_locales/nn/messages.json +++ b/apps/browser/src/_locales/nn/messages.json @@ -3358,20 +3358,6 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Auto-fill suggestions" }, @@ -3630,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/or/messages.json b/apps/browser/src/_locales/or/messages.json index f66a8d3c08a..fa76cf7060a 100644 --- a/apps/browser/src/_locales/or/messages.json +++ b/apps/browser/src/_locales/or/messages.json @@ -3358,20 +3358,6 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Auto-fill suggestions" }, @@ -3630,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/pl/messages.json b/apps/browser/src/_locales/pl/messages.json index 0ca85d73680..1603314a616 100644 --- a/apps/browser/src/_locales/pl/messages.json +++ b/apps/browser/src/_locales/pl/messages.json @@ -3358,20 +3358,6 @@ "passkeyRemoved": { "message": "Passkey został usunięty" }, - "unassignedItemsBannerNotice": { - "message": "Uwaga: Nieprzypisane elementy organizacji nie są już widoczne w widoku Wszystkie sejfy i są teraz dostępne tylko przez Konsolę Administracyjną." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Uwaga: 16 maja 2024 r. nieprzypisana elementy w organizacji nie będą już widoczne w widoku Wszystkie sejfy i będą dostępne tylko przez Konsolę Administracyjną." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Przypisz te elementy do kolekcji z", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": ", aby były widoczne.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Sugestie autouzupełnienia" }, @@ -3630,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/pt_BR/messages.json b/apps/browser/src/_locales/pt_BR/messages.json index c8b665fda5b..a9161bde22c 100644 --- a/apps/browser/src/_locales/pt_BR/messages.json +++ b/apps/browser/src/_locales/pt_BR/messages.json @@ -3358,20 +3358,6 @@ "passkeyRemoved": { "message": "Chave de acesso removida" }, - "unassignedItemsBannerNotice": { - "message": "Aviso: Itens da organização não atribuídos não estão mais visíveis na visualização Todos os Cofres e só são acessíveis por meio do painel de administração." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Aviso: Em 16 de maio, 2024, itens da organização não serão mais visíveis na visualização Todos os Cofres e só serão acessíveis por meio do painel de administração." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Atribua estes itens a uma coleção da", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "para torná-los visíveis.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Sugestões de autopreenchimento" }, @@ -3630,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/pt_PT/messages.json b/apps/browser/src/_locales/pt_PT/messages.json index a2fe3571eda..dc64dfa8a7d 100644 --- a/apps/browser/src/_locales/pt_PT/messages.json +++ b/apps/browser/src/_locales/pt_PT/messages.json @@ -572,13 +572,13 @@ "message": "Ocorreu um erro" }, "emailRequired": { - "message": "É necessário o endereço de e-mail." + "message": "O endereço de e-mail é obrigatório." }, "invalidEmail": { "message": "Endereço de e-mail inválido." }, "masterPasswordRequired": { - "message": "É necessária a palavra-passe mestra." + "message": "A palavra-passe mestra é obrigatória." }, "confirmMasterPasswordRequired": { "message": "É necessário reescrever a palavra-passe mestra." @@ -661,7 +661,7 @@ "message": "Ocorreu um erro inesperado." }, "nameRequired": { - "message": "É necessário o nome." + "message": "O nome é obrigatório." }, "addedFolder": { "message": "Pasta adicionada" @@ -2763,10 +2763,10 @@ "message": "Dispositivo de confiança" }, "inputRequired": { - "message": "Campo necessário." + "message": "Campo obrigatório." }, "required": { - "message": "necessário" + "message": "obrigatório" }, "search": { "message": "Procurar" @@ -3358,20 +3358,6 @@ "passkeyRemoved": { "message": "Chave de acesso removida" }, - "unassignedItemsBannerNotice": { - "message": "Aviso: Os itens da organização não atribuídos já não são visíveis na vista Todos os cofres e só são acessíveis através da Consola de administração." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Aviso: A 16 de maio de 2024, os itens da organização não atribuídos deixarão de estar visíveis na vista Todos os cofres e só estarão acessíveis através da consola de administração." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Atribua estes itens a uma coleção a partir da", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "para os tornar visíveis.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Sugestões de preenchimento automático" }, @@ -3630,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/ro/messages.json b/apps/browser/src/_locales/ro/messages.json index 211a587cfaa..3b8809f22dc 100644 --- a/apps/browser/src/_locales/ro/messages.json +++ b/apps/browser/src/_locales/ro/messages.json @@ -3358,20 +3358,6 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Auto-fill suggestions" }, @@ -3630,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/ru/messages.json b/apps/browser/src/_locales/ru/messages.json index 3c4d24b3ddf..e5b303c7ea6 100644 --- a/apps/browser/src/_locales/ru/messages.json +++ b/apps/browser/src/_locales/ru/messages.json @@ -3358,20 +3358,6 @@ "passkeyRemoved": { "message": "Passkey удален" }, - "unassignedItemsBannerNotice": { - "message": "Уведомление: Неприсвоенные элементы организации больше не видны в представлении \"Все хранилища\" и доступны только через консоль администратора." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Уведомление: с 16 мая 2024 года не назначенные элементы организации больше не будут видны в представлении \"Все хранилища\" и будут доступны только через консоль администратора." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Назначьте эти элементы в коллекцию из", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "чтобы сделать их видимыми.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Предложения по автозаполнению" }, @@ -3630,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/si/messages.json b/apps/browser/src/_locales/si/messages.json index b6f6de8107e..1416bea9582 100644 --- a/apps/browser/src/_locales/si/messages.json +++ b/apps/browser/src/_locales/si/messages.json @@ -3358,20 +3358,6 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Auto-fill suggestions" }, @@ -3630,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/sk/messages.json b/apps/browser/src/_locales/sk/messages.json index d125d30b634..c375af5ffa9 100644 --- a/apps/browser/src/_locales/sk/messages.json +++ b/apps/browser/src/_locales/sk/messages.json @@ -3358,20 +3358,6 @@ "passkeyRemoved": { "message": "Prístupový kľúč bol odstránený" }, - "unassignedItemsBannerNotice": { - "message": "Upozornenie: Nepriradené položky organizácie už nie sú viditeľné v zobrazení Všetky trezory a sú prístupné iba cez Správcovskú konzolu." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Upozornenie: 16. mája 2024 nepriradené položky organizácie už nebudú viditeľné v zobrazení Všetky trezory a budú prístupné iba cez Správcovskú konzolu." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Priradiť tieto položky do zbierky zo", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": ", aby boli viditeľné.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Návrhy automatického vypĺňania" }, @@ -3630,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/sl/messages.json b/apps/browser/src/_locales/sl/messages.json index a7bc089c69f..e53e830b006 100644 --- a/apps/browser/src/_locales/sl/messages.json +++ b/apps/browser/src/_locales/sl/messages.json @@ -3358,20 +3358,6 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Auto-fill suggestions" }, @@ -3630,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/sr/messages.json b/apps/browser/src/_locales/sr/messages.json index 2fadd93d1d4..02a3f824521 100644 --- a/apps/browser/src/_locales/sr/messages.json +++ b/apps/browser/src/_locales/sr/messages.json @@ -1484,7 +1484,7 @@ } }, "viewItemHeader": { - "message": "View $TYPE$", + "message": "Видети $TYPE$", "placeholders": { "type": { "content": "$1", @@ -3358,20 +3358,6 @@ "passkeyRemoved": { "message": "Приступачни кључ је уклоњен" }, - "unassignedItemsBannerNotice": { - "message": "Напомена: Недодељене ставке организације више нису видљиве у приказу Сви сефови и доступне су само преко Админ конзоле." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Напомена: од 16 Маја 2024м недодељене ставке организације више нису видљиве у приказу Сви сефови и доступне су само преко Админ конзоле." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Предлози за ауто-попуњавање" }, @@ -3545,22 +3531,22 @@ "message": "Није могуће приступити ставкама у деактивираним организацијама. Обратите се власнику ваше организације за помоћ." }, "additionalInformation": { - "message": "Additional information" + "message": "Додатне информације" }, "itemHistory": { - "message": "Item history" + "message": "Историја предмета" }, "lastEdited": { - "message": "Last edited" + "message": "Последња измена" }, "ownerYou": { - "message": "Owner: You" + "message": "Власник: Ви" }, "linked": { - "message": "Linked" + "message": "Повезано" }, "copySuccessful": { - "message": "Copy Successful" + "message": "Копија успешна" }, "upload": { "message": "Отпреми" @@ -3602,16 +3588,16 @@ "message": "Филтери" }, "personalDetails": { - "message": "Personal details" + "message": "Личне информације" }, "identification": { - "message": "Identification" + "message": "Идентификација" }, "contactInfo": { - "message": "Contact info" + "message": "Контакт инфо" }, "downloadAttachment": { - "message": "Download - $ITEMNAME$", + "message": "Преузми - $ITEMNAME$", "placeholders": { "itemname": { "content": "$1", @@ -3630,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/sv/messages.json b/apps/browser/src/_locales/sv/messages.json index 439c7be920d..3444a42c364 100644 --- a/apps/browser/src/_locales/sv/messages.json +++ b/apps/browser/src/_locales/sv/messages.json @@ -3358,20 +3358,6 @@ "passkeyRemoved": { "message": "Passkey borttagen" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "för att göra dem synliga.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Auto-fill suggestions" }, @@ -3630,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/te/messages.json b/apps/browser/src/_locales/te/messages.json index f66a8d3c08a..fa76cf7060a 100644 --- a/apps/browser/src/_locales/te/messages.json +++ b/apps/browser/src/_locales/te/messages.json @@ -3358,20 +3358,6 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Auto-fill suggestions" }, @@ -3630,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/th/messages.json b/apps/browser/src/_locales/th/messages.json index 9b02c5b8fb9..8cd62631f36 100644 --- a/apps/browser/src/_locales/th/messages.json +++ b/apps/browser/src/_locales/th/messages.json @@ -3358,20 +3358,6 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Auto-fill suggestions" }, @@ -3630,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/tr/messages.json b/apps/browser/src/_locales/tr/messages.json index 40133762ab9..4af23e28a36 100644 --- a/apps/browser/src/_locales/tr/messages.json +++ b/apps/browser/src/_locales/tr/messages.json @@ -3358,20 +3358,6 @@ "passkeyRemoved": { "message": "Geçiş anahtarı kaldırıldı" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Önerileri otomatik doldur" }, @@ -3630,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/uk/messages.json b/apps/browser/src/_locales/uk/messages.json index 45f8e12a291..fe9fde369ac 100644 --- a/apps/browser/src/_locales/uk/messages.json +++ b/apps/browser/src/_locales/uk/messages.json @@ -3358,20 +3358,6 @@ "passkeyRemoved": { "message": "Ключ доступу вилучено" }, - "unassignedItemsBannerNotice": { - "message": "Примітка: непризначені елементи організації більше не видимі у поданні \"Усі сховища\" і доступні лише в консолі адміністратора." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Примітка: 16 травня 2024 року непризначені елементи організації більше не будуть видимі у поданні \"Усі сховища\" і будуть доступні лише через консоль адміністратора." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Призначте ці елементи збірці в", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "щоб зробити їх видимими.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Пропозиції автозаповнення" }, @@ -3630,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/vi/messages.json b/apps/browser/src/_locales/vi/messages.json index 23dea4cdd79..3210c43acbf 100644 --- a/apps/browser/src/_locales/vi/messages.json +++ b/apps/browser/src/_locales/vi/messages.json @@ -3358,20 +3358,6 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Lưu ý: Các mục tổ chức chưa được chỉ định sẽ không còn hiển thị trong chế độ xem Tất cả Vault và chỉ có thể truy cập được qua Bảng điều khiển dành cho quản trị viên." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Lưu ý: Vào ngày 16 tháng 5 năm 2024, các mục tổ chức chưa được chỉ định sẽ không còn hiển thị trong chế độ xem Tất cả Vault và sẽ chỉ có thể truy cập được qua Bảng điều khiển dành cho quản trị viên." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Gán các mục này vào một bộ sưu tập từ", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "để làm cho chúng hiển thị.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Auto-fill suggestions" }, @@ -3630,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/zh_CN/messages.json b/apps/browser/src/_locales/zh_CN/messages.json index 7eae4db0ce8..afc990ea68d 100644 --- a/apps/browser/src/_locales/zh_CN/messages.json +++ b/apps/browser/src/_locales/zh_CN/messages.json @@ -3358,20 +3358,6 @@ "passkeyRemoved": { "message": "通行密钥已移除" }, - "unassignedItemsBannerNotice": { - "message": "注意:未分配的组织项目在「所有密码库」视图中不再可见,只能通过管理控制台访问。" - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "注意:从 2024 年 5 月 16 日起,未分配的组织项目在「所有密码库」视图中将不再可见,只能通过管理控制台访问。" - }, - "unassignedItemsBannerCTAPartOne": { - "message": "将这些项目分配到集合,通过", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": ",以使其可见。", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "自动填充建议" }, @@ -3630,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/zh_TW/messages.json b/apps/browser/src/_locales/zh_TW/messages.json index 0dd36e8ac9c..37436357245 100644 --- a/apps/browser/src/_locales/zh_TW/messages.json +++ b/apps/browser/src/_locales/zh_TW/messages.json @@ -3358,20 +3358,6 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Auto-fill suggestions" }, @@ -3630,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } From 5969f24254df79709761b9646bd909b5013da53a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 15 Jul 2024 12:55:34 +0200 Subject: [PATCH 24/57] Autosync the updated translations (#10115) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/desktop/src/locales/fi/messages.json | 10 +++++----- apps/desktop/src/locales/pt_PT/messages.json | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/desktop/src/locales/fi/messages.json b/apps/desktop/src/locales/fi/messages.json index 40a87ebc28c..2da6d22607c 100644 --- a/apps/desktop/src/locales/fi/messages.json +++ b/apps/desktop/src/locales/fi/messages.json @@ -540,16 +540,16 @@ } }, "masterPassword": { - "message": "Master password" + "message": "Pääsalasana" }, "masterPassImportant": { - "message": "Your master password cannot be recovered if you forget it!" + "message": "Pääsalasanasi palauttaminen ei ole mahdollista, jos unohdat sen!" }, "confirmMasterPassword": { - "message": "Confirm master password" + "message": "Vahvista pääsalasana" }, "masterPassHintLabel": { - "message": "Master password hint" + "message": "Pääsalasanan vihje" }, "settings": { "message": "Asetukset" @@ -1968,7 +1968,7 @@ "message": "Sähköpostiosoite on vahvistettava" }, "emailVerifiedV2": { - "message": "Email verified" + "message": "Sähköpostiosoite on vahvistettu" }, "emailVerificationRequiredDesc": { "message": "Sinun on vahvistettava sähköpostiosoitteesi käyttääksesi tätä ominaisuutta." diff --git a/apps/desktop/src/locales/pt_PT/messages.json b/apps/desktop/src/locales/pt_PT/messages.json index dda760c5ff8..dc7a6226849 100644 --- a/apps/desktop/src/locales/pt_PT/messages.json +++ b/apps/desktop/src/locales/pt_PT/messages.json @@ -346,7 +346,7 @@ "message": "Remover" }, "nameRequired": { - "message": "É necessário o nome." + "message": "O nome é obrigatório." }, "addedItem": { "message": "Item adicionado" @@ -2621,10 +2621,10 @@ "message": "Dispositivo de confiança" }, "inputRequired": { - "message": "Campo necessário." + "message": "Campo obrigatório." }, "required": { - "message": "necessário" + "message": "obrigatório" }, "search": { "message": "Procurar" From a1667e56030f3740636536aa25f513685bbae2cf Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 15 Jul 2024 11:00:21 +0000 Subject: [PATCH 25/57] Autosync the updated translations (#10118) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/web/src/locales/af/messages.json | 17 ------- apps/web/src/locales/ar/messages.json | 17 ------- apps/web/src/locales/az/messages.json | 31 +++--------- apps/web/src/locales/be/messages.json | 17 ------- apps/web/src/locales/bg/messages.json | 17 ------- apps/web/src/locales/bn/messages.json | 17 ------- apps/web/src/locales/bs/messages.json | 17 ------- apps/web/src/locales/ca/messages.json | 17 ------- apps/web/src/locales/cs/messages.json | 17 ------- apps/web/src/locales/cy/messages.json | 17 ------- apps/web/src/locales/da/messages.json | 17 ------- apps/web/src/locales/de/messages.json | 25 ++-------- apps/web/src/locales/el/messages.json | 17 ------- apps/web/src/locales/en_GB/messages.json | 19 +------- apps/web/src/locales/en_IN/messages.json | 19 +------- apps/web/src/locales/eo/messages.json | 17 ------- apps/web/src/locales/es/messages.json | 17 ------- apps/web/src/locales/et/messages.json | 17 ------- apps/web/src/locales/eu/messages.json | 17 ------- apps/web/src/locales/fa/messages.json | 17 ------- apps/web/src/locales/fi/messages.json | 61 +++++++++--------------- apps/web/src/locales/fil/messages.json | 17 ------- apps/web/src/locales/fr/messages.json | 17 ------- apps/web/src/locales/gl/messages.json | 17 ------- apps/web/src/locales/he/messages.json | 17 ------- apps/web/src/locales/hi/messages.json | 17 ------- apps/web/src/locales/hr/messages.json | 17 ------- apps/web/src/locales/hu/messages.json | 25 ++-------- apps/web/src/locales/id/messages.json | 17 ------- apps/web/src/locales/it/messages.json | 17 ------- apps/web/src/locales/ja/messages.json | 17 ------- apps/web/src/locales/ka/messages.json | 17 ------- apps/web/src/locales/km/messages.json | 17 ------- apps/web/src/locales/kn/messages.json | 17 ------- apps/web/src/locales/ko/messages.json | 17 ------- apps/web/src/locales/lv/messages.json | 17 ------- apps/web/src/locales/ml/messages.json | 17 ------- apps/web/src/locales/mr/messages.json | 17 ------- apps/web/src/locales/my/messages.json | 17 ------- apps/web/src/locales/nb/messages.json | 17 ------- apps/web/src/locales/ne/messages.json | 17 ------- apps/web/src/locales/nl/messages.json | 17 ------- apps/web/src/locales/nn/messages.json | 17 ------- apps/web/src/locales/or/messages.json | 17 ------- apps/web/src/locales/pl/messages.json | 17 ------- apps/web/src/locales/pt_BR/messages.json | 17 ------- apps/web/src/locales/pt_PT/messages.json | 35 ++++---------- apps/web/src/locales/ro/messages.json | 17 ------- apps/web/src/locales/ru/messages.json | 17 ------- apps/web/src/locales/si/messages.json | 17 ------- apps/web/src/locales/sk/messages.json | 17 ------- apps/web/src/locales/sl/messages.json | 17 ------- apps/web/src/locales/sr/messages.json | 35 ++++---------- apps/web/src/locales/sr_CS/messages.json | 17 ------- apps/web/src/locales/sv/messages.json | 17 ------- apps/web/src/locales/te/messages.json | 17 ------- apps/web/src/locales/th/messages.json | 17 ------- apps/web/src/locales/tr/messages.json | 17 ------- apps/web/src/locales/uk/messages.json | 17 ------- apps/web/src/locales/vi/messages.json | 17 ------- apps/web/src/locales/zh_CN/messages.json | 19 +------- apps/web/src/locales/zh_TW/messages.json | 17 ------- 62 files changed, 58 insertions(+), 1112 deletions(-) diff --git a/apps/web/src/locales/af/messages.json b/apps/web/src/locales/af/messages.json index b17efb1b04a..9809c1cc7f1 100644 --- a/apps/web/src/locales/af/messages.json +++ b/apps/web/src/locales/af/messages.json @@ -8161,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "U kan nie uself tot ’n groep toevoeg nie." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Skrap verskaffer" }, diff --git a/apps/web/src/locales/ar/messages.json b/apps/web/src/locales/ar/messages.json index 9b0bac34052..6974170dd65 100644 --- a/apps/web/src/locales/ar/messages.json +++ b/apps/web/src/locales/ar/messages.json @@ -8161,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "You cannot add yourself to a group." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Delete provider" }, diff --git a/apps/web/src/locales/az/messages.json b/apps/web/src/locales/az/messages.json index 4a30eb3c63d..d49fd446241 100644 --- a/apps/web/src/locales/az/messages.json +++ b/apps/web/src/locales/az/messages.json @@ -1060,7 +1060,7 @@ "message": "Davam etmək istədiyinizə əminsiniz?" }, "moveSelectedItemsDesc": { - "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", + "message": "Seçilmiş $COUNT$ elementi əlavə etmək istədiyiniz qovluğu seçin.", "placeholders": { "count": { "content": "$1", @@ -7883,7 +7883,7 @@ "message": "Bu kolleksiyalara təyin et" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Only organization members with access to these collections will be able to see the items." + "message": "Yalnız bu kolleksiyalara müraciəti olan təşkilat üzvləri bu elementləri görə biləcək." }, "selectCollectionsToAssign": { "message": "Təyin ediləcək kolleksiyaları seçin" @@ -8161,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "Özünüzü bir qrupa əlavə edə bilməzsiniz." }, - "unassignedItemsBannerSelfHost": { - "message": "Bildiriş: 2 May 2024-cü ildən etibarən təyin edilməmiş təşkilat elementləri artıq cihazlar arasında Bütün Anbarlar görünüşündə görünməyən və yalnız Admin Konsolu vasitəsilə əlçatan olacaq. Bu elementləri görünən etmək üçün Admin Konsolundan bir kolleksiyaya təyin edin." - }, - "unassignedItemsBannerNotice": { - "message": "Bildiriş: Təyin edilməyən təşkilat elementləri artıq Bütün Anbarlar görünüşündə görünən olmayacaq və yalnız Admin Konsolu vasitəsilə əlçatan olacaq." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Bildiriş: 16 May 2024-cü il tarixindən etibarən, təyin edilməyən təşkilat elementləri cihazlar arasında və Bütün Anbarlar görünüşündə görünən olmayacaq və yalnız Admin Konsolu vasitəsilə əlçatan olacaq." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Bu elementləri görünən etmək üçün", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "bir kolleksiyaya təyin edin.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Provayderi sil" }, @@ -8546,16 +8529,16 @@ "message": "Sponsorlu" }, "licenseAndBillingManagementDesc": { - "message": "After making updates in the Bitwarden cloud server, upload your license file to apply the most recent changes." + "message": "Bitwarden bulud serverində güncəlləmələr etdikdən sonra, ən son dəyişiklikləri tətbiq etmək üçün lisenziya faylınızı yükləyin." }, "addToFolder": { - "message": "Add to folder" + "message": "Qovluğa əlavə et" }, "selectFolder": { - "message": "Select folder" + "message": "Qovluq seç" }, "personalItemsTransferWarning": { - "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "message": "$PERSONAL_ITEMS_COUNT$ seçilmiş təşkilata birdəfəlik transfer ediləcək. Artıq bu elementlərə sahib olmaya bilməyəcəksiniz.", "placeholders": { "personal_items_count": { "content": "$1", @@ -8564,7 +8547,7 @@ } }, "personalItemsWithOrgTransferWarning": { - "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "message": "$PERSONAL_ITEMS_COUNT$, $ORG$ təşkilatına birdəfəlik transfer ediləcək. Artıq bu elementlərə sahib olmaya bilməyəcəksiniz.", "placeholders": { "personal_items_count": { "content": "$1", diff --git a/apps/web/src/locales/be/messages.json b/apps/web/src/locales/be/messages.json index d1334b431c3..ba53bcc52e4 100644 --- a/apps/web/src/locales/be/messages.json +++ b/apps/web/src/locales/be/messages.json @@ -8161,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "You cannot add yourself to a group." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Delete provider" }, diff --git a/apps/web/src/locales/bg/messages.json b/apps/web/src/locales/bg/messages.json index e7719e3d57c..9d7e581c3f7 100644 --- a/apps/web/src/locales/bg/messages.json +++ b/apps/web/src/locales/bg/messages.json @@ -8161,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "Не може да добавяте себе си към групи." }, - "unassignedItemsBannerSelfHost": { - "message": "Известие: от 2 май 2024г. неразпределените елементи на организациите вече няма се виждат в изгледа с „Всички трезори“ на различните устройства, а ще бъдат достъпни само през Административната конзола. Добавете тези елементи към някоя колекция в Административната конзола, за да станат видими." - }, - "unassignedItemsBannerNotice": { - "message": "Известие: неразпределените елементи в организацията вече няма да се виждат в изгледа с всички трезори на различните устройства, а са достъпни само през Административната конзола." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Известие: след 16 май 2024, неразпределените елементи в организацията вече няма да се виждат в изгледа с всички трезори на различните устройства, а ще бъдат достъпни само през Административната конзола." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Добавете тези елементи към колекция в", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "за да ги направите видими.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Изтриване на доставчик" }, diff --git a/apps/web/src/locales/bn/messages.json b/apps/web/src/locales/bn/messages.json index c07835d7dc4..962b70c0839 100644 --- a/apps/web/src/locales/bn/messages.json +++ b/apps/web/src/locales/bn/messages.json @@ -8161,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "You cannot add yourself to a group." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Delete provider" }, diff --git a/apps/web/src/locales/bs/messages.json b/apps/web/src/locales/bs/messages.json index 5485a872aeb..198150c31de 100644 --- a/apps/web/src/locales/bs/messages.json +++ b/apps/web/src/locales/bs/messages.json @@ -8161,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "You cannot add yourself to a group." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Delete provider" }, diff --git a/apps/web/src/locales/ca/messages.json b/apps/web/src/locales/ca/messages.json index 51eeff484dd..bba01123c09 100644 --- a/apps/web/src/locales/ca/messages.json +++ b/apps/web/src/locales/ca/messages.json @@ -8161,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "No podeu afegir-vos vosaltres mateix a un grup." }, - "unassignedItemsBannerSelfHost": { - "message": "Avís: el 2 de maig de 2024, els elements de l'organització no assignats deixaran de ser visibles a la vista \"Totes les caixes fortes\" en tots els dispositius i només es podran accedir des de la Consola d'administració. Assigna aquests elements a una col·lecció des de la Consola d'administració per fer-los visibles." - }, - "unassignedItemsBannerNotice": { - "message": "Avís: els elements de l'organització no assignats ja no són visibles a la visualització de Totes les caixes fortes en tots els dispositius i ara només es poden accedir des de la Consola d'administració." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Avís: el 16 de maig de 2024, els elements de l'organització no assignats deixaran de ser visibles a la visualització de Totes les caixes fortes en tots els dispositius i només es podran accedir des de la Consola d'administració." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assigna aquests elements a una col·lecció de", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "per fer-los visibles.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Suprimeix proveïdor" }, diff --git a/apps/web/src/locales/cs/messages.json b/apps/web/src/locales/cs/messages.json index 581345d155c..31efa53c824 100644 --- a/apps/web/src/locales/cs/messages.json +++ b/apps/web/src/locales/cs/messages.json @@ -8161,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "Do skupiny nemůžete přidat sami sebe." }, - "unassignedItemsBannerSelfHost": { - "message": "Upozornění: Dne 2. května 2024 již nebudou nepřiřazené položky organizace viditelné v zobrazení Všechny trezory ve všech zařízeních a budou přístupné jen prostřednictvím konzoly správce. Přiřaďte tyto položky do kolekce z konzoly pro správce, aby byly viditelné." - }, - "unassignedItemsBannerNotice": { - "message": "Upozornění: Nepřiřazené položky organizace již nejsou viditelné ve Vašem zobrazení všech trezorů napříč zařízeními a jsou nyní přístupné pouze v konzoli správce." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Upozornění: 16. květba 2024 již nebudou nepřiřazené položky organizace viditelné ve Vašem zobrazení všech trezorů napříč zařízeními a budou přístupné pouze v konzoli správce." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Přiřadit tyto položky ke kolekci z", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "aby byly viditelné.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Smazat poskytovatele" }, diff --git a/apps/web/src/locales/cy/messages.json b/apps/web/src/locales/cy/messages.json index 4244b3f41c0..6ebf3c97f8f 100644 --- a/apps/web/src/locales/cy/messages.json +++ b/apps/web/src/locales/cy/messages.json @@ -8161,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "You cannot add yourself to a group." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Delete provider" }, diff --git a/apps/web/src/locales/da/messages.json b/apps/web/src/locales/da/messages.json index 44b4e775af5..1d64000aad4 100644 --- a/apps/web/src/locales/da/messages.json +++ b/apps/web/src/locales/da/messages.json @@ -8161,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "Man kan ikke føje sig selv til en gruppe." }, - "unassignedItemsBannerSelfHost": { - "message": "Bemærk: Pr. 2. maj 2024 vil utildelte organisationsemner ikke længere være synlige i Alle Bokse-visningen på tværs af enheder og vil kun være tilgængelige via Admin-konsollen. Tildel disse emner til en samling via Admin-konsollen for at gøre dem synlige." - }, - "unassignedItemsBannerNotice": { - "message": "Bemærk: Utildelte organisationsemner er ikke længere synlige i Alle Bokse-visningen, men er kun tilgængelige via Admin-konsol." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Bemærk: Pr. 16. maj 2024 er utildelte organisationsemner er ikke længere synlige i Alle Bokse-visningen, men er kun tilgængelige via Admin-konsol." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Tildel disse emner til en samling via", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "for at gøre dem synlige.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Slet udbyder" }, diff --git a/apps/web/src/locales/de/messages.json b/apps/web/src/locales/de/messages.json index ab55254dfbd..808956a4db3 100644 --- a/apps/web/src/locales/de/messages.json +++ b/apps/web/src/locales/de/messages.json @@ -8161,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "Du kannst dich nicht selbst zu einer Gruppe hinzufügen." }, - "unassignedItemsBannerSelfHost": { - "message": "Hinweis: Ab dem 2. Mai 2024 werden nicht zugewiesene Organisationseinträge nicht mehr geräteübergreifend in der Ansicht aller Tresore sichtbar sein und sind nur über die Administrator-Konsole zugänglich. Weise diese Elemente einer Sammlung aus der Administrator-Konsole zu, um sie sichtbar zu machen." - }, - "unassignedItemsBannerNotice": { - "message": "Hinweis: Nicht zugewiesene Organisationseinträge sind nicht mehr geräteübergreifend in der Ansicht aller Tresore sichtbar und nun nur über die Administrator-Konsole zugänglich." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Hinweis: Ab dem 16. Mai 2024 werden nicht zugewiesene Organisationseinträge nicht mehr geräteübergreifend in der Ansicht aller Tresore sichtbar sein und nur über die Administrator-Konsole zugänglich." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Weise diese Einträge einer Sammlung aus der", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "zu, um sie sichtbar zu machen.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Anbieter löschen" }, @@ -8549,13 +8532,13 @@ "message": "Lade nach der Durchführung von Aktualisierungen im Bitwarden Cloud Server deine Lizenzdatei hoch, um die neuesten Änderungen anzuwenden." }, "addToFolder": { - "message": "Add to folder" + "message": "Zu Ordner hinzufügen" }, "selectFolder": { - "message": "Select folder" + "message": "Ordner auswählen" }, "personalItemsTransferWarning": { - "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "message": "$PERSONAL_ITEMS_COUNT$ werden dauerhaft an die ausgewählte Organisation übertragen. Du wirst diese Einträge nicht mehr besitzen.", "placeholders": { "personal_items_count": { "content": "$1", @@ -8564,7 +8547,7 @@ } }, "personalItemsWithOrgTransferWarning": { - "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "message": "$PERSONAL_ITEMS_COUNT$ werden dauerhaft an $ORG$ übertragen. Du wirst diese Einträge nicht mehr besitzen.", "placeholders": { "personal_items_count": { "content": "$1", diff --git a/apps/web/src/locales/el/messages.json b/apps/web/src/locales/el/messages.json index a33b71b1917..6ce73ff9532 100644 --- a/apps/web/src/locales/el/messages.json +++ b/apps/web/src/locales/el/messages.json @@ -8161,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "You cannot add yourself to a group." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Delete provider" }, diff --git a/apps/web/src/locales/en_GB/messages.json b/apps/web/src/locales/en_GB/messages.json index a6036f687c1..fd5f3b28293 100644 --- a/apps/web/src/locales/en_GB/messages.json +++ b/apps/web/src/locales/en_GB/messages.json @@ -7883,7 +7883,7 @@ "message": "Assign to these collections" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Only organization members with access to these collections will be able to see the items." + "message": "Only organisation members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Select collections to assign" @@ -8161,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "You cannot add yourself to a group." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organisation items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organisation items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organisation items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Delete provider" }, diff --git a/apps/web/src/locales/en_IN/messages.json b/apps/web/src/locales/en_IN/messages.json index 38fd6bdc5f7..e21f70f68ee 100644 --- a/apps/web/src/locales/en_IN/messages.json +++ b/apps/web/src/locales/en_IN/messages.json @@ -7883,7 +7883,7 @@ "message": "Assign to these collections" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Only organization members with access to these collections will be able to see the items." + "message": "Only organisation members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Select collections to assign" @@ -8161,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "You cannot add yourself to a group." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On 2 May 2024, unassigned organisation items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organisation items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On 16 May 2024, unassigned organisation items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Delete provider" }, diff --git a/apps/web/src/locales/eo/messages.json b/apps/web/src/locales/eo/messages.json index b84133ad0df..5150b57946d 100644 --- a/apps/web/src/locales/eo/messages.json +++ b/apps/web/src/locales/eo/messages.json @@ -8161,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "You cannot add yourself to a group." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Delete provider" }, diff --git a/apps/web/src/locales/es/messages.json b/apps/web/src/locales/es/messages.json index cc52cf68df2..97a0dc44f73 100644 --- a/apps/web/src/locales/es/messages.json +++ b/apps/web/src/locales/es/messages.json @@ -8161,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "You cannot add yourself to a group." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Delete provider" }, diff --git a/apps/web/src/locales/et/messages.json b/apps/web/src/locales/et/messages.json index aa27b64bc8b..d166e1747e7 100644 --- a/apps/web/src/locales/et/messages.json +++ b/apps/web/src/locales/et/messages.json @@ -8161,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "You cannot add yourself to a group." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Delete provider" }, diff --git a/apps/web/src/locales/eu/messages.json b/apps/web/src/locales/eu/messages.json index e8713254735..1220a5dc637 100644 --- a/apps/web/src/locales/eu/messages.json +++ b/apps/web/src/locales/eu/messages.json @@ -8161,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "You cannot add yourself to a group." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Delete provider" }, diff --git a/apps/web/src/locales/fa/messages.json b/apps/web/src/locales/fa/messages.json index 50da37fea51..3156ac2b889 100644 --- a/apps/web/src/locales/fa/messages.json +++ b/apps/web/src/locales/fa/messages.json @@ -8161,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "You cannot add yourself to a group." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Delete provider" }, diff --git a/apps/web/src/locales/fi/messages.json b/apps/web/src/locales/fi/messages.json index 3516862c78f..eb7fcb5ba0b 100644 --- a/apps/web/src/locales/fi/messages.json +++ b/apps/web/src/locales/fi/messages.json @@ -412,7 +412,7 @@ "message": "Kohteen nimi" }, "cannotRemoveViewOnlyCollections": { - "message": "Et voi poistaa kokoelmia Vain katselu -oikeuksilla: $COLLECTIONS$", + "message": "Et voi poistaa kokoelmia, joihin sinulla on vain tarkasteluoikeus: $COLLECTIONS$", "placeholders": { "collections": { "content": "$1", @@ -855,7 +855,7 @@ "message": "Sähköpostiosoite" }, "yourVaultIsLockedV2": { - "message": "Your vault is locked" + "message": "Holvisi on lukittu" }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Haluatko varmasti jatkaa?" }, "moveSelectedItemsDesc": { - "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", + "message": "Valitse kansio, johon haluat lisätä $COUNT$ kohteen/kohdetta.", "placeholders": { "count": { "content": "$1", @@ -1692,7 +1692,7 @@ "message": "Skannaa alla oleva QR-koodi todennussovelluksellasi tai syötä avain." }, "twoStepAuthenticatorQRCanvasError": { - "message": "Could not load QR code. Try again or use the key below." + "message": "QR-koodin lataus ei onnistunut. Yritä uudelleen tai käytä alla olevaa avainta." }, "key": { "message": "Avain" @@ -3403,7 +3403,7 @@ "message": "Tilin sähköpostiosoite on vahvistettu" }, "emailVerifiedV2": { - "message": "Email verified" + "message": "Sähköpostiosoite on vahvistettu" }, "emailVerifiedFailed": { "message": "Sähköpostiosoitettasi ei voitu vahvistaa. Yritä lähettää uusi vahvistussähköposti." @@ -3712,7 +3712,7 @@ } }, "subscriptionSeatMaxReached": { - "message": "Voit kutsua enintään $COUNT$ jäsentä kasvattamatta tilauksesi käyttäjäpaikkojen määrää.", + "message": "Voit kutsua enintään $COUNT$ jäsentä korottamatta tilauksesi käyttäjäpaikkojen määrää.", "placeholders": { "count": { "content": "$1", @@ -7883,7 +7883,7 @@ "message": "Määritä seuraaviin kokoelmiin" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Only organization members with access to these collections will be able to see the items." + "message": "Vain näiden kokoelmien käyttöoikeuden omaavat organisaation jäsenet voivat nähdä kohteet." }, "selectCollectionsToAssign": { "message": "Valitse määritettävät kokoelmat" @@ -8161,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "Et voi lisätä itseäsi ryhmään." }, - "unassignedItemsBannerSelfHost": { - "message": "Huomioi: Alkaen 2. toukokuuta 2024, määrittämättömät organisaatiokohteet eivät enää näy laitteidesi \"Kaikki holvit\" -näkymissä, vaan ne ovat käytettävissä vain Hallintapaneelista." - }, - "unassignedItemsBannerNotice": { - "message": "Huomioi: Määrittämättömät organisaatiokohteet eivät enää näy laitteidesi \"Kaikki holvit\" -näkymissä, vaan ne ovat käytettävissä vain Hallintapaneelista." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Huomioi: Alkaen 16. toukokuuta 2024, määrittämättömät organisaatiokohteet eivät enää näy laitteidesi \"Kaikki holvit\" -näkymissä, vaan ne ovat käytettävissä vain Hallintapaneelista." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Määritä nämä kohteet kokoelmaan", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "ista, jotta ne näkyvät.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Poista toimittaja" }, @@ -8349,10 +8332,10 @@ } }, "viewInfo": { - "message": "Tarkastele tietoja" + "message": "Näytä tiedot" }, "viewAccess": { - "message": "Tarkastele oikeuksia" + "message": "Näytä oikeudet" }, "noCollectionsSelected": { "message": "Et ole valinnut yhtään kokoelmaa." @@ -8421,7 +8404,7 @@ "message": "Näytä salaisuus" }, "noClients": { - "message": "Näytettäviä päätteitä ei ole" + "message": "Näytettäviä asiakkaita ei ole" }, "providerBillingEmailHint": { "message": "Tämä sähköpostiosoite vastaanottaa kaikki tätä toimittajaa koskevat laskut", @@ -8446,7 +8429,7 @@ "message": "Varmista, että jäsenillä on oikeiden käyttäjätietojen käyttöoikeus ja että heidän tilinsä on suojattu. Tämän raportin avulla saat CSV-tiedoston käyttäjien oikeuksista ja tiliasetuksista." }, "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": "Tarkastele organisaation jäsenten käyttöoikeuksia ryhmiin, kokoelmiin ja kokoelmien kohteisiin. CSV-vienti tuottaa yksityiskohtaisen jäsenkohtaisen erittelyn, joka sisältää kokoelmaoikeudet ja tilimääritykset." }, "higherKDFIterations": { "message": "Korkeampi KDF-toistojen määrä vahvistaa pääsalasanasi suojausta väsytyshyökkäyksien varalta." @@ -8521,41 +8504,41 @@ "message": "Lataa CSV" }, "monthlySubscriptionUserSeatsMessage": { - "message": "Adjustments to your subscription will result in prorated charges to your billing totals on your next billing period. " + "message": "Tilausmuutoksista aiheutuvat laskutussumman muutokset huomioidaan suhteutetusti seuraavan laskutuskauden veloituksessa." }, "annualSubscriptionUserSeatsMessage": { - "message": "Adjustments to your subscription will result in prorated charges on a monthly billing cycle. " + "message": "Tilausmuutoksista aiheutuvat laskutussumman muutokset huomioidaan suhteutetusti kuukausilaskutuksen seuraavassa veloituksessa." }, "billingHistoryDescription": { "message": "Lataa CSV-tiedosto nähdäksesi jokaisen laskutuspäivän asiakastiedot. Korjattuja veloituksia ei sisällytetä CSV-teidostoon ja ne saattavat poiketa liitetystä laskusta. Tarkimmat laskutustiedot näet kuukausilaskuistasi.", "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": { - "message": "Listattavia laskuja ei ole", + "message": "Näytettäviä laskuja ei ole", "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": "Huomautus: Asiakasholvien yksityisyyttä parannetaan myöhemmin tässä kuussa, jonka jälkeen toimittajan jäsenillä ei ole enää käyttöoikeutta niiden sisältämiin kohteisiin. Lisätietoja saat", "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": { - "message": "contact Bitwarden support.", + "message": "Bitwardenin asiakaspalvelusta.", "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'. 'Bitwarden' should not be translated" }, "sponsored": { - "message": "Sponsored" + "message": "Sponsoroitu" }, "licenseAndBillingManagementDesc": { - "message": "After making updates in the Bitwarden cloud server, upload your license file to apply the most recent changes." + "message": "Ota uusimmat muutokset käyttöön lataamalla lisenssitiedostosi Bitwardenin pilvipalvelimen muutosten jälkeen." }, "addToFolder": { - "message": "Add to folder" + "message": "Lisää kansioon" }, "selectFolder": { - "message": "Select folder" + "message": "Valitse kansio" }, "personalItemsTransferWarning": { - "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "message": "$PERSONAL_ITEMS_COUNT$ siirretään pysyvästi valittulle organisaatiolle, etkä enää omista näitä kohteita.", "placeholders": { "personal_items_count": { "content": "$1", @@ -8564,7 +8547,7 @@ } }, "personalItemsWithOrgTransferWarning": { - "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "message": "$PERSONAL_ITEMS_COUNT$ siirretään pysyvästi organisaatiolle $ORG$, etkä enää omista näitä kohteita.", "placeholders": { "personal_items_count": { "content": "$1", diff --git a/apps/web/src/locales/fil/messages.json b/apps/web/src/locales/fil/messages.json index 6c72fd4d3ba..a89ecc6d2e3 100644 --- a/apps/web/src/locales/fil/messages.json +++ b/apps/web/src/locales/fil/messages.json @@ -8161,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "You cannot add yourself to a group." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Delete provider" }, diff --git a/apps/web/src/locales/fr/messages.json b/apps/web/src/locales/fr/messages.json index 3f9e475ba75..ee1c81c74b6 100644 --- a/apps/web/src/locales/fr/messages.json +++ b/apps/web/src/locales/fr/messages.json @@ -8161,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "Vous ne pouvez pas vous ajouter vous-même à un groupe." }, - "unassignedItemsBannerSelfHost": { - "message": "Remarque : le 2 mai 2024, les éléments d'organisation non assignés ne seront plus visibles dans votre vue Tous les coffres sur les appareils et seront uniquement accessibles via la Console Admin. Assignez ces éléments à une collection à partir de la Console Admin pour les rendre visibles." - }, - "unassignedItemsBannerNotice": { - "message": "Remarque : Les éléments d'organisation non assignés ne sont plus visibles dans la vue Tous les coffres sur les appareils et ne sont maintenant accessibles que via la Console Admin." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Remarque : À partir du 16 mai 2024, les éléments d'organisation non assignés ne seront plus visibles dans la vue Tous les coffres sur les appareils et ne seront maintenant accessibles que via la Console Admin." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assigner ces éléments à une collection depuis", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "pour les rendre visibles.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Supprimer le fournisseur" }, diff --git a/apps/web/src/locales/gl/messages.json b/apps/web/src/locales/gl/messages.json index 2d8503f35b8..a0393577921 100644 --- a/apps/web/src/locales/gl/messages.json +++ b/apps/web/src/locales/gl/messages.json @@ -8161,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "You cannot add yourself to a group." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Delete provider" }, diff --git a/apps/web/src/locales/he/messages.json b/apps/web/src/locales/he/messages.json index 0bfc56c60c3..a5ae8083f2b 100644 --- a/apps/web/src/locales/he/messages.json +++ b/apps/web/src/locales/he/messages.json @@ -8161,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "You cannot add yourself to a group." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Delete provider" }, diff --git a/apps/web/src/locales/hi/messages.json b/apps/web/src/locales/hi/messages.json index 2a9f25a0e1f..fa4e16d6ea6 100644 --- a/apps/web/src/locales/hi/messages.json +++ b/apps/web/src/locales/hi/messages.json @@ -8161,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "You cannot add yourself to a group." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Delete provider" }, diff --git a/apps/web/src/locales/hr/messages.json b/apps/web/src/locales/hr/messages.json index c6901bd70c7..8e9ca538308 100644 --- a/apps/web/src/locales/hr/messages.json +++ b/apps/web/src/locales/hr/messages.json @@ -8161,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "You cannot add yourself to a group." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Delete provider" }, diff --git a/apps/web/src/locales/hu/messages.json b/apps/web/src/locales/hu/messages.json index dacc855d7b1..02f64df294c 100644 --- a/apps/web/src/locales/hu/messages.json +++ b/apps/web/src/locales/hu/messages.json @@ -8161,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "Nem adhadjuk magunkat a csoporthoz." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerNotice": { - "message": "Megjegyzés: A nem hozzárendelt szervezeti elemek már nem láthatók az Összes széf nézetben a különböző eszközökön és csak az Adminisztrátori konzolon keresztül érhetők el." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Megjegyzés: 2024. május 16-tól a nem hozzárendelt szervezeti elemek többé nem lesznek láthatók az Összes széf nézetben a különböző eszközökön és csak az Adminisztrátoir konzolon keresztül lesznek elérhetők." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Rendeljük hozzá ezeket az elemeket a gyűjteményhez", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "a láthatósághoz.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Delete provider" }, @@ -8549,13 +8532,13 @@ "message": "A Bitwarden felhőkiszolgáló frissítése után töltsük fel a licenszfájlt a legutóbbi módosítások alkalmazásához." }, "addToFolder": { - "message": "Add to folder" + "message": "Mappához adás" }, "selectFolder": { - "message": "Select folder" + "message": "Mappa kiválasztása" }, "personalItemsTransferWarning": { - "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "message": "$PERSONAL_ITEMS_COUNT$ véglegesen átkerül a kiválasztott szervezethez. A továbbiakban nem leszünk a tulajdonosa ezeknek az elemeknek.", "placeholders": { "personal_items_count": { "content": "$1", @@ -8564,7 +8547,7 @@ } }, "personalItemsWithOrgTransferWarning": { - "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "message": "$PERSONAL_ITEMS_COUNT$ véglegesen átkerül $ORG$ szervezeti egységbe. A továbbiakban nem leszünk a tulajdonosa ezeknek az elemeknek.", "placeholders": { "personal_items_count": { "content": "$1", diff --git a/apps/web/src/locales/id/messages.json b/apps/web/src/locales/id/messages.json index a12e45cddfd..c6370a50ab2 100644 --- a/apps/web/src/locales/id/messages.json +++ b/apps/web/src/locales/id/messages.json @@ -8161,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "You cannot add yourself to a group." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Delete provider" }, diff --git a/apps/web/src/locales/it/messages.json b/apps/web/src/locales/it/messages.json index 59332396b64..34d9512210e 100644 --- a/apps/web/src/locales/it/messages.json +++ b/apps/web/src/locales/it/messages.json @@ -8161,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "Non puoi aggiungerti a un gruppo." }, - "unassignedItemsBannerSelfHost": { - "message": "Avviso: dal 2 maggio 2024, gli elementi dell'organizzazione non assegnati non saranno più visibili nella tua visualizzazione Tutte le Cassaforti su tutti i dispositivi e saranno accessibili solo tramite la Console di amministrazione. Assegna questi elementi ad una raccolta dalla Console di amministrazione per renderli visibili." - }, - "unassignedItemsBannerNotice": { - "message": "Avviso: gli elementi dell'organizzazione non assegnati non sono più visibili nella visualizzazione Tutte le Cassaforti su tutti i dispositivi e sono ora accessibili solo tramite la Console di amministrazione." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Avviso: dal 2 maggio 2024, gli elementi dell'organizzazione non assegnati non saranno più visibili nella tua visualizzazione Tutte le Cassaforti su tutti i dispositivi e saranno accessibili solo tramite la Console di amministrazione." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assegna questi elementi ad una raccolta dalla", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "per renderli visibili.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Elimina fornitore" }, diff --git a/apps/web/src/locales/ja/messages.json b/apps/web/src/locales/ja/messages.json index 53db2390d1c..e8ba94d82bd 100644 --- a/apps/web/src/locales/ja/messages.json +++ b/apps/web/src/locales/ja/messages.json @@ -8161,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "あなた自身をグループに追加することはできません。" }, - "unassignedItemsBannerSelfHost": { - "message": "お知らせ:2024年5月2日に、 割り当てられていない組織アイテムはデバイス間のすべての保管庫ビューに表示されなくなり、管理コンソールからのみアクセス可能になります。 管理コンソールからコレクションにこれらのアイテムを割り当てると、表示できるようになります。" - }, - "unassignedItemsBannerNotice": { - "message": "注意: 割り当てられていない組織アイテムは、デバイス間のすべての保管庫ビューでは表示されなくなり、管理コンソールからのみアクセスできるようになりました。" - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "お知らせ:2024年5月16日に、 割り当てられていない組織アイテムは、デバイス間のすべての保管庫ビューに表示されなくなり、管理コンソールからのみアクセス可能になります。" - }, - "unassignedItemsBannerCTAPartOne": { - "message": "これらのアイテムのコレクションへの割り当てを", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "で実行すると表示できるようになります。", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "プロバイダを削除" }, diff --git a/apps/web/src/locales/ka/messages.json b/apps/web/src/locales/ka/messages.json index 1a160bd22ac..cb76f9ad569 100644 --- a/apps/web/src/locales/ka/messages.json +++ b/apps/web/src/locales/ka/messages.json @@ -8161,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "You cannot add yourself to a group." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Delete provider" }, diff --git a/apps/web/src/locales/km/messages.json b/apps/web/src/locales/km/messages.json index 2d8503f35b8..a0393577921 100644 --- a/apps/web/src/locales/km/messages.json +++ b/apps/web/src/locales/km/messages.json @@ -8161,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "You cannot add yourself to a group." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Delete provider" }, diff --git a/apps/web/src/locales/kn/messages.json b/apps/web/src/locales/kn/messages.json index 4cbbd331350..2b9b8a3806e 100644 --- a/apps/web/src/locales/kn/messages.json +++ b/apps/web/src/locales/kn/messages.json @@ -8161,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "You cannot add yourself to a group." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Delete provider" }, diff --git a/apps/web/src/locales/ko/messages.json b/apps/web/src/locales/ko/messages.json index 98df1b9e04a..0fb2f23a524 100644 --- a/apps/web/src/locales/ko/messages.json +++ b/apps/web/src/locales/ko/messages.json @@ -8161,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "You cannot add yourself to a group." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Delete provider" }, diff --git a/apps/web/src/locales/lv/messages.json b/apps/web/src/locales/lv/messages.json index c8e23e455c0..e69ebf72e9c 100644 --- a/apps/web/src/locales/lv/messages.json +++ b/apps/web/src/locales/lv/messages.json @@ -8161,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "Sevi nevar pievienot kopai." }, - "unassignedItemsBannerSelfHost": { - "message": "Jāņem vērā: no 2024. gada 2. maija nepiešķirti apvienības vienumi vairs nebūs redzami skatā \"Visas glabātavas\" dažādās ierīcēs un būs sasniedzami tikai no pārvaldības konsoles, kur šie vienumi jāpiešķir krājumam, lai padarītu tos redzamus." - }, - "unassignedItemsBannerNotice": { - "message": "Jāņem vērā: nepiešķirti apvienības vienumi vairs nav redzami skatā \"Visas glabātavas\" dažādās ierīcēs un tagad ir pieejami tikai pārvaldības konsolē." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Jāņem vērā: no 2024. gada 16. maija nepiešķirti apvienības vienumi vairs nebūs redzami skatā \"Visas glabātavas\" dažādās ierīcēs un būs pieejami tikai pārvaldības konsolē." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Piešķirt šos vienumus krājumam", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": ", lai padarītu tos redzamus.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Izdzēst nodrošinātāju" }, diff --git a/apps/web/src/locales/ml/messages.json b/apps/web/src/locales/ml/messages.json index 48b91ad1db6..ddab9ea2a9f 100644 --- a/apps/web/src/locales/ml/messages.json +++ b/apps/web/src/locales/ml/messages.json @@ -8161,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "You cannot add yourself to a group." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Delete provider" }, diff --git a/apps/web/src/locales/mr/messages.json b/apps/web/src/locales/mr/messages.json index 2d8503f35b8..a0393577921 100644 --- a/apps/web/src/locales/mr/messages.json +++ b/apps/web/src/locales/mr/messages.json @@ -8161,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "You cannot add yourself to a group." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Delete provider" }, diff --git a/apps/web/src/locales/my/messages.json b/apps/web/src/locales/my/messages.json index 2d8503f35b8..a0393577921 100644 --- a/apps/web/src/locales/my/messages.json +++ b/apps/web/src/locales/my/messages.json @@ -8161,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "You cannot add yourself to a group." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Delete provider" }, diff --git a/apps/web/src/locales/nb/messages.json b/apps/web/src/locales/nb/messages.json index b97916165e6..68c70f67682 100644 --- a/apps/web/src/locales/nb/messages.json +++ b/apps/web/src/locales/nb/messages.json @@ -8161,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "You cannot add yourself to a group." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Delete provider" }, diff --git a/apps/web/src/locales/ne/messages.json b/apps/web/src/locales/ne/messages.json index 7a9f598015c..333d5a6f87f 100644 --- a/apps/web/src/locales/ne/messages.json +++ b/apps/web/src/locales/ne/messages.json @@ -8161,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "You cannot add yourself to a group." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Delete provider" }, diff --git a/apps/web/src/locales/nl/messages.json b/apps/web/src/locales/nl/messages.json index d804d74df65..d24ce9e5a3b 100644 --- a/apps/web/src/locales/nl/messages.json +++ b/apps/web/src/locales/nl/messages.json @@ -8161,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "Je kunt jezelf niet aan een groep toevoegen." }, - "unassignedItemsBannerSelfHost": { - "message": "Kennisgeving: Vanaf 2 mei 2024 zijn niet-toegewezen organisatie-items op geen enkel apparaat meer zichtbaar in de weergave van alle kluisjes en alleen toegankelijk via de Admin Console. Je kunt deze items in het Admin Console aan een collectie toewijzen om ze zichtbaar te maken." - }, - "unassignedItemsBannerNotice": { - "message": "Let op: Niet-toegewezen organisatie-items zijn niet langer zichtbaar in de weergave van alle kluissen op verschillende apparaten en zijn nu alleen toegankelijk via de Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Let op: Vanaf 16 mei 2024 zijn niet-toegewezen organisatie-items niet langer zichtbaar in de weergave van alle kluissen op verschillende apparaten en alleen toegankelijk via de Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Deze items toewijzen aan een collectie van de", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "om ze zichtbaar te maken.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Provider verwijderen" }, diff --git a/apps/web/src/locales/nn/messages.json b/apps/web/src/locales/nn/messages.json index cc07d6e9b82..7ac77cec177 100644 --- a/apps/web/src/locales/nn/messages.json +++ b/apps/web/src/locales/nn/messages.json @@ -8161,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "You cannot add yourself to a group." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Delete provider" }, diff --git a/apps/web/src/locales/or/messages.json b/apps/web/src/locales/or/messages.json index 2d8503f35b8..a0393577921 100644 --- a/apps/web/src/locales/or/messages.json +++ b/apps/web/src/locales/or/messages.json @@ -8161,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "You cannot add yourself to a group." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Delete provider" }, diff --git a/apps/web/src/locales/pl/messages.json b/apps/web/src/locales/pl/messages.json index 4b33f28ab74..f5a39ccce9f 100644 --- a/apps/web/src/locales/pl/messages.json +++ b/apps/web/src/locales/pl/messages.json @@ -8161,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "Nie możesz dodać siebie do grupy." }, - "unassignedItemsBannerSelfHost": { - "message": "Uwaga: 2 maja 2024 r. nieprzypisane elementy w organizacji nie będą już widoczne w widoku Wszystkie sejfy na urządzeniach i będą dostępne tylko przez Konsolę Administracyjną. Przypisz te elementy do kolekcji z konsoli administracyjnej, aby były one widoczne." - }, - "unassignedItemsBannerNotice": { - "message": "Uwaga: Nieprzypisane elementy organizacji nie są już widoczne w widoku Wszystkie sejfy na urządzeniach i są teraz dostępne tylko przez Konsolę Administracyjną." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Uwaga: 16 maja 2024 r. nieprzypisana elementy w organizacji nie będą już widoczne w widoku Wszystkie sejfy na urządzeniach i będą dostępne tylko przez Konsolę Administracyjną." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Przypisz te elementy do kolekcji z", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": ", aby były widoczne.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Usuń dostawcę" }, diff --git a/apps/web/src/locales/pt_BR/messages.json b/apps/web/src/locales/pt_BR/messages.json index 72240aa94a5..ea8ea2db5f4 100644 --- a/apps/web/src/locales/pt_BR/messages.json +++ b/apps/web/src/locales/pt_BR/messages.json @@ -8161,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "Você não pode adicionar você mesmo a um grupo." }, - "unassignedItemsBannerSelfHost": { - "message": "Aviso: Em 2 de maio de 2024, itens da organização não estarão mais visíveis em sua visualização de Todos os Cofres entre dispositivos e só serão acessíveis por meio do painel de administração. Atribuir estes itens a uma coleção do Console de Administração para torná-los visíveis." - }, - "unassignedItemsBannerNotice": { - "message": "Aviso: Itens de organização não atribuídos não estão mais visíveis na sua tela Todos os Cofres através dos dispositivos e agora só são acessíveis por meio do Console de Administração." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Aviso: Em 16 de maio, 2024, itens da organização que não foram atribuídos não estarão mais visíveis em sua visualização de Todos os Cofres dos dispositivos e só serão acessíveis por meio do painel de administração." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Atribua estes itens a uma coleção da", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "para torná-los visíveis.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Excluir Provedor" }, diff --git a/apps/web/src/locales/pt_PT/messages.json b/apps/web/src/locales/pt_PT/messages.json index 200bac58516..af5f1fb4000 100644 --- a/apps/web/src/locales/pt_PT/messages.json +++ b/apps/web/src/locales/pt_PT/messages.json @@ -812,13 +812,13 @@ "message": "Obter a dica da palavra-passe mestra" }, "emailRequired": { - "message": "É necessário o endereço de e-mail." + "message": "O endereço de e-mail é obrigatório." }, "invalidEmail": { "message": "Endereço de e-mail inválido." }, "masterPasswordRequired": { - "message": "É necessária a palavra-passe mestra." + "message": "A palavra-passe mestra é obrigatória." }, "confirmMasterPasswordRequired": { "message": "É necessário reescrever a palavra-passe mestra." @@ -855,7 +855,7 @@ "message": "Endereço de e-mail" }, "yourVaultIsLockedV2": { - "message": "O seu cofre está bloqueado." + "message": "O seu cofre está bloqueado" }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Tem a certeza de que deseja continuar?" }, "moveSelectedItemsDesc": { - "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", + "message": "Escolha uma pasta à qual pretende adicionar o(s) $COUNT$ item(ns) selecionado(s).", "placeholders": { "count": { "content": "$1", @@ -5742,7 +5742,7 @@ "message": "1 campo acima precisa da sua atenção." }, "fieldRequiredError": { - "message": "$FIELDNAME$ é necessário.", + "message": "$FIELDNAME$ obrigatório.", "placeholders": { "fieldname": { "content": "$1", @@ -5751,7 +5751,7 @@ } }, "required": { - "message": "necessário" + "message": "obrigatório" }, "charactersCurrentAndMaximum": { "message": "$CURRENT$/$MAX$ máximo de caracteres", @@ -6139,7 +6139,7 @@ "description": "the text, 'SCIM', is an acronymn and should not be translated." }, "inputRequired": { - "message": "Campo necessário." + "message": "Campo obrigatório." }, "inputEmail": { "message": "O campo não é um endereço de e-mail." @@ -6673,7 +6673,7 @@ "description": "A unique string that gives a client application (eg. CLI) access to a secret or set of secrets." }, "accessTokenExpirationRequired": { - "message": "Prazo de validade necessário", + "message": "Prazo de validade obrigatório", "description": "Error message indicating that an expiration date for the access token must be set." }, "accessTokenCreatedAndCopied": { @@ -7883,7 +7883,7 @@ "message": "Atribuir a estas coleções" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Only organization members with access to these collections will be able to see the items." + "message": "Apenas os membros da organização com acesso a estas coleções poderão ver os itens." }, "selectCollectionsToAssign": { "message": "Selecione as coleções a atribuir" @@ -8161,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "Não se pode adicionar a si próprio a um grupo." }, - "unassignedItemsBannerSelfHost": { - "message": "Aviso: A 2 de maio de 2024, os itens da organização não atribuídos deixarão de estar visíveis na vista Todos os cofres em todos os dispositivos e só estarão acessíveis através da Consola de administração. Atribua estes itens a uma coleção a partir da Consola de administração para os tornar visíveis." - }, - "unassignedItemsBannerNotice": { - "message": "Aviso: Os itens da organização não atribuídos já não são visíveis na vista Todos os cofres em todos os dispositivos e agora só estão acessíveis através da Consola de administração." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Aviso: A 16 de maio de 2024, os itens da organização não atribuídos deixarão de estar visíveis na vista Todos os cofres em todos os dispositivos e só estarão acessíveis através da Consola de administração." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Atribua estes itens a uma coleção a partir da", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "para os tornar visíveis.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Eliminar fornecedor" }, diff --git a/apps/web/src/locales/ro/messages.json b/apps/web/src/locales/ro/messages.json index d31aae4c89e..421b3ac2720 100644 --- a/apps/web/src/locales/ro/messages.json +++ b/apps/web/src/locales/ro/messages.json @@ -8161,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "You cannot add yourself to a group." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Delete provider" }, diff --git a/apps/web/src/locales/ru/messages.json b/apps/web/src/locales/ru/messages.json index 482cd72e25f..33f943c3d0a 100644 --- a/apps/web/src/locales/ru/messages.json +++ b/apps/web/src/locales/ru/messages.json @@ -8161,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "Нельзя добавить самого себя в группу." }, - "unassignedItemsBannerSelfHost": { - "message": "Уведомление: 2 мая 2024 года неприсвоенные элементы организации больше не будут отображаться в представлении \"Все хранилища\" на всех устройствах и будут доступны только через консоль администратора. Назначьте эти элементы коллекции в консоли администратора, чтобы сделать их видимыми." - }, - "unassignedItemsBannerNotice": { - "message": "Уведомление: Неприсвоенные элементы организации больше не отображаются в представлении \"Все хранилища\" на всех устройствах и теперь доступны только через консоль администратора." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Уведомление: с 16 мая 2024 года неназначенные элементы организации больше не будут отображаться в представлении \"Все хранилища\" на всех устройствах и будут доступны только через консоль администратора." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Назначьте эти элементы в коллекцию из", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "чтобы сделать их видимыми.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Удалить провайдера" }, diff --git a/apps/web/src/locales/si/messages.json b/apps/web/src/locales/si/messages.json index 308eeaebe68..c750bb57545 100644 --- a/apps/web/src/locales/si/messages.json +++ b/apps/web/src/locales/si/messages.json @@ -8161,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "You cannot add yourself to a group." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Delete provider" }, diff --git a/apps/web/src/locales/sk/messages.json b/apps/web/src/locales/sk/messages.json index ab0dc33a0ba..933014f8726 100644 --- a/apps/web/src/locales/sk/messages.json +++ b/apps/web/src/locales/sk/messages.json @@ -8161,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "You cannot add yourself to a group." }, - "unassignedItemsBannerSelfHost": { - "message": "Upozornenie: 2. mája nepriradené položky organizácie už nebudú viditeľné v zobrazení Všetky Trezory a budú prístupné len cez administrátorskú konzolu. Aby boli viditeľné, priraďte tieto položky do kolekcie z konzoly administrátora." - }, - "unassignedItemsBannerNotice": { - "message": "Upozornenie: Nepriradené položky organizácie už nie sú viditeľné v zobrazení Všetky trezory a sú prístupné iba cez Správcovskú konzolu." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Upozornenie: 16. mája 2024 nepriradené položky organizácie už nebudú viditeľné v zobrazení Všetky trezory a budú prístupné iba cez Správcovskú konzolu." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Priradiť tieto položky do zbierky zo", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": ", aby boli viditeľné.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Odstrániť poskytovateľa" }, diff --git a/apps/web/src/locales/sl/messages.json b/apps/web/src/locales/sl/messages.json index 85eeb907ddc..ed777b1251d 100644 --- a/apps/web/src/locales/sl/messages.json +++ b/apps/web/src/locales/sl/messages.json @@ -8161,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "You cannot add yourself to a group." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Delete provider" }, diff --git a/apps/web/src/locales/sr/messages.json b/apps/web/src/locales/sr/messages.json index 9453c73567d..c0e487db8e0 100644 --- a/apps/web/src/locales/sr/messages.json +++ b/apps/web/src/locales/sr/messages.json @@ -855,7 +855,7 @@ "message": "Имејл" }, "yourVaultIsLockedV2": { - "message": "Your vault is locked" + "message": "Ваш сеф је блокиран" }, "uuid": { "message": "UUID" @@ -1692,7 +1692,7 @@ "message": "Скенирајте КР кôд у наставку помоћу апликације за аутентификацију или унесите кључ." }, "twoStepAuthenticatorQRCanvasError": { - "message": "Could not load QR code. Try again or use the key below." + "message": "Учитавање QR кôда није успело. Покушајте поново или користите тастер испод." }, "key": { "message": "Кључ" @@ -8161,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "Не можете да се додате у групу." }, - "unassignedItemsBannerSelfHost": { - "message": "Обавештење: 2. маја 2024. недодељене ставке организације више неће бити видљиве у вашем приказу Сви сефови на свим уређајима и биће им доступне само преко Админ конзоле. Доделите ове ставке колекцији са Админ конзолом да бисте их учинили видљивим." - }, - "unassignedItemsBannerNotice": { - "message": "Напомена: Недодељене ставке организације више нису видљиве у вашем приказу Сви сефови на свим уређајима и сада су доступне само преко Админ конзоле." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Обавештење: 16. маја 2024. недодељене ставке организације више неће бити видљиве у вашем приказу Сви сефови на свим уређајима и биће им доступне само преко Админ конзоле." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Избриши провајдера" }, @@ -8446,7 +8429,7 @@ "message": "Уверите се да чланови имају приступ правим акредитивима и да су њихови налози сигурни. Користите овај извештај да бисте добили ЦСВ приступ чланова и конфигурације налога." }, "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": "Провера приступа чланова организације кроз групе, колекције и ставке колекције. ЦСВ извоз пружа детаљну анализу по члану, укључујући информације о дозволама за прикупљање и конфигурацијама налога." }, "higherKDFIterations": { "message": "Веће KDF итерације може помоћи у заштити ваше главне лозинке од грубе присиле од стране нападача." @@ -8543,19 +8526,19 @@ "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'. 'Bitwarden' should not be translated" }, "sponsored": { - "message": "Sponsored" + "message": "Спонзорисано" }, "licenseAndBillingManagementDesc": { - "message": "After making updates in the Bitwarden cloud server, upload your license file to apply the most recent changes." + "message": "Након ажурирања на Bitwarden клауду серверу, отпремите датотеку лиценце да бисте применили најновије промене." }, "addToFolder": { - "message": "Add to folder" + "message": "Додај фасцикли" }, "selectFolder": { - "message": "Select folder" + "message": "Изабери фасциклу" }, "personalItemsTransferWarning": { - "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "message": "$PERSONAL_ITEMS_COUNT$ биће трајно пребачени у изабрану организацију. Више нећете имати ове ставке.", "placeholders": { "personal_items_count": { "content": "$1", @@ -8564,7 +8547,7 @@ } }, "personalItemsWithOrgTransferWarning": { - "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "message": "$PERSONAL_ITEMS_COUNT$ биће трајно пребачени у $ORG$. Више нећете имати ове ставке.", "placeholders": { "personal_items_count": { "content": "$1", diff --git a/apps/web/src/locales/sr_CS/messages.json b/apps/web/src/locales/sr_CS/messages.json index b5cc6b22a28..574f1c9a3dd 100644 --- a/apps/web/src/locales/sr_CS/messages.json +++ b/apps/web/src/locales/sr_CS/messages.json @@ -8161,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "You cannot add yourself to a group." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Delete provider" }, diff --git a/apps/web/src/locales/sv/messages.json b/apps/web/src/locales/sv/messages.json index b3692022bd9..206bd766dc7 100644 --- a/apps/web/src/locales/sv/messages.json +++ b/apps/web/src/locales/sv/messages.json @@ -8161,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "Du kan inte lägga till dig själv i en grupp." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Radera leverantör" }, diff --git a/apps/web/src/locales/te/messages.json b/apps/web/src/locales/te/messages.json index 2d8503f35b8..a0393577921 100644 --- a/apps/web/src/locales/te/messages.json +++ b/apps/web/src/locales/te/messages.json @@ -8161,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "You cannot add yourself to a group." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Delete provider" }, diff --git a/apps/web/src/locales/th/messages.json b/apps/web/src/locales/th/messages.json index 40b78db0099..e2a456a04ef 100644 --- a/apps/web/src/locales/th/messages.json +++ b/apps/web/src/locales/th/messages.json @@ -8161,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "You cannot add yourself to a group." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Delete provider" }, diff --git a/apps/web/src/locales/tr/messages.json b/apps/web/src/locales/tr/messages.json index cc2d4a7d4c4..27bd265dd99 100644 --- a/apps/web/src/locales/tr/messages.json +++ b/apps/web/src/locales/tr/messages.json @@ -8161,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "Kendinizi gruba ekleyemezsiniz." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Delete provider" }, diff --git a/apps/web/src/locales/uk/messages.json b/apps/web/src/locales/uk/messages.json index 76cefd8bfe5..e316cc0322d 100644 --- a/apps/web/src/locales/uk/messages.json +++ b/apps/web/src/locales/uk/messages.json @@ -8161,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "Ви не можете додати себе до групи." }, - "unassignedItemsBannerSelfHost": { - "message": "Сповіщення: 2 травня 2024 року, непризначені елементи організації більше не будуть видимі на ваших пристроях у поданні \"Усі сховища\", і будуть доступні лише через консоль адміністратора. Щоб зробити їх видимими, призначте ці елементи збірці в консолі адміністратора." - }, - "unassignedItemsBannerNotice": { - "message": "Примітка: непризначені елементи організації більше не видимі на ваших пристроях у поданні \"Усі сховища\", і тепер доступні лише через консоль адміністратора." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Примітка: 16 травня 2024 року непризначені елементи організації більше не будуть видимі на ваших пристроях у поданні \"Усі сховища\", і будуть доступні лише через консоль адміністратора." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Призначте ці елементи збірці в", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "щоб зробити їх видимими.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Видалити провайдера" }, diff --git a/apps/web/src/locales/vi/messages.json b/apps/web/src/locales/vi/messages.json index 2969940f86e..7f61058de48 100644 --- a/apps/web/src/locales/vi/messages.json +++ b/apps/web/src/locales/vi/messages.json @@ -8161,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "You cannot add yourself to a group." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Delete provider" }, diff --git a/apps/web/src/locales/zh_CN/messages.json b/apps/web/src/locales/zh_CN/messages.json index 8030c4986fc..462dc05dbfd 100644 --- a/apps/web/src/locales/zh_CN/messages.json +++ b/apps/web/src/locales/zh_CN/messages.json @@ -1060,7 +1060,7 @@ "message": "确定要继续吗?" }, "moveSelectedItemsDesc": { - "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", + "message": "选择一个您想要将这 $COUNT$ 个所选项目添加到的文件夹。", "placeholders": { "count": { "content": "$1", @@ -8161,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "您不能将自己添加到群组。" }, - "unassignedItemsBannerSelfHost": { - "message": "注意:从 2024 年 5 月 2 日起,未分配的组织项目在您所有设备的「所有密码库」视图中将不再可见,只能通过管理控制台访问。通过管理控制台将这些项目分配给集合以使其可见。" - }, - "unassignedItemsBannerNotice": { - "message": "注意:未分配的组织项目在您所有设备的「所有密码库」视图中不再可见,现在只能通过管理控制台访问。" - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "注意:从 2024 年 5 月 2 日起,未分配的组织项目在您所有设备的「所有密码库」视图中将不再可见,只能通过管理控制台访问。" - }, - "unassignedItemsBannerCTAPartOne": { - "message": "将这些项目分配到集合,通过", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": ",以使其可见。", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "删除提供商" }, diff --git a/apps/web/src/locales/zh_TW/messages.json b/apps/web/src/locales/zh_TW/messages.json index 2bd991eabb9..7120eb05db9 100644 --- a/apps/web/src/locales/zh_TW/messages.json +++ b/apps/web/src/locales/zh_TW/messages.json @@ -8161,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "You cannot add yourself to a group." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "刪除提供者" }, From 5a46c7d5cc27ace85c93b371e4327012386afae3 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Mon, 15 Jul 2024 15:59:39 +0200 Subject: [PATCH 26/57] [PM-7084] 4/6: Introduce shared email two-factor component (#9770) * Add shared email two-factor component * Update apps/browser/src/auth/popup/two-factor-auth-email.component.ts Co-authored-by: Jake Fink --------- Co-authored-by: Jake Fink --- .../popup/two-factor-auth-email.component.ts | 55 +++++++++ .../auth/popup/two-factor-auth.component.ts | 3 + .../src/auth/two-factor-auth.component.ts | 2 + .../src/app/auth/two-factor-auth.component.ts | 2 + .../two-factor-auth-email.component.html | 19 +++ .../two-factor-auth-email.component.ts | 109 ++++++++++++++++++ .../two-factor-auth.component.html | 4 + .../two-factor-auth.component.ts | 2 + 8 files changed, 196 insertions(+) create mode 100644 apps/browser/src/auth/popup/two-factor-auth-email.component.ts create mode 100644 libs/angular/src/auth/components/two-factor-auth/two-factor-auth-email.component.html create mode 100644 libs/angular/src/auth/components/two-factor-auth/two-factor-auth-email.component.ts diff --git a/apps/browser/src/auth/popup/two-factor-auth-email.component.ts b/apps/browser/src/auth/popup/two-factor-auth-email.component.ts new file mode 100644 index 00000000000..e865435b8b4 --- /dev/null +++ b/apps/browser/src/auth/popup/two-factor-auth-email.component.ts @@ -0,0 +1,55 @@ +import { DialogModule } from "@angular/cdk/dialog"; +import { CommonModule } from "@angular/common"; +import { Component, inject } from "@angular/core"; +import { ReactiveFormsModule, FormsModule } from "@angular/forms"; + +import { TwoFactorAuthEmailComponent as TwoFactorAuthEmailBaseComponent } from "@bitwarden/angular/auth/components/two-factor-auth/two-factor-auth-email.component"; +import { JslibModule } from "@bitwarden/angular/jslib.module"; + +import { AsyncActionsModule } from "../../../../../libs/components/src/async-actions"; +import { ButtonModule } from "../../../../../libs/components/src/button"; +import { DialogService } from "../../../../../libs/components/src/dialog"; +import { FormFieldModule } from "../../../../../libs/components/src/form-field"; +import { LinkModule } from "../../../../../libs/components/src/link"; +import { I18nPipe } from "../../../../../libs/components/src/shared/i18n.pipe"; +import { TypographyModule } from "../../../../../libs/components/src/typography"; +import BrowserPopupUtils from "../../platform/popup/browser-popup-utils"; + +@Component({ + standalone: true, + selector: "app-two-factor-auth-email", + templateUrl: + "../../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-auth-email.component.html", + imports: [ + CommonModule, + JslibModule, + DialogModule, + ButtonModule, + LinkModule, + TypographyModule, + ReactiveFormsModule, + FormFieldModule, + AsyncActionsModule, + FormsModule, + ], + providers: [I18nPipe], +}) +export class TwoFactorAuthEmailComponent extends TwoFactorAuthEmailBaseComponent { + private dialogService = inject(DialogService); + + async ngOnInit(): Promise { + if (BrowserPopupUtils.inPopup(window)) { + const confirmed = await this.dialogService.openSimpleDialog({ + title: { key: "warning" }, + content: { key: "popup2faCloseMessage" }, + type: "warning", + }); + if (confirmed) { + await BrowserPopupUtils.openCurrentPagePopout(window); + return; + } + } + + await super.ngOnInit(); + } +} diff --git a/apps/browser/src/auth/popup/two-factor-auth.component.ts b/apps/browser/src/auth/popup/two-factor-auth.component.ts index 67ff0fd2857..23251e2d58a 100644 --- a/apps/browser/src/auth/popup/two-factor-auth.component.ts +++ b/apps/browser/src/auth/popup/two-factor-auth.component.ts @@ -41,6 +41,8 @@ import { import { BrowserApi } from "../../platform/browser/browser-api"; import BrowserPopupUtils from "../../platform/popup/browser-popup-utils"; +import { TwoFactorAuthEmailComponent } from "./two-factor-auth-email.component"; + @Component({ standalone: true, templateUrl: @@ -59,6 +61,7 @@ import BrowserPopupUtils from "../../platform/popup/browser-popup-utils"; RouterLink, CheckboxModule, TwoFactorOptionsComponent, + TwoFactorAuthEmailComponent, TwoFactorAuthAuthenticatorComponent, TwoFactorAuthYubikeyComponent, ], diff --git a/apps/desktop/src/auth/two-factor-auth.component.ts b/apps/desktop/src/auth/two-factor-auth.component.ts index 191a88e621a..a07509527e8 100644 --- a/apps/desktop/src/auth/two-factor-auth.component.ts +++ b/apps/desktop/src/auth/two-factor-auth.component.ts @@ -5,6 +5,7 @@ import { ReactiveFormsModule } from "@angular/forms"; import { RouterLink } from "@angular/router"; import { TwoFactorAuthAuthenticatorComponent } from "../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-auth-authenticator.component"; +import { TwoFactorAuthEmailComponent } from "../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-auth-email.component"; import { TwoFactorAuthYubikeyComponent } from "../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-auth-yubikey.component"; import { TwoFactorAuthComponent as BaseTwoFactorAuthComponent } from "../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-auth.component"; import { TwoFactorOptionsComponent } from "../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-options.component"; @@ -35,6 +36,7 @@ import { TypographyModule } from "../../../../libs/components/src/typography"; RouterLink, CheckboxModule, TwoFactorOptionsComponent, + TwoFactorAuthEmailComponent, TwoFactorAuthAuthenticatorComponent, TwoFactorAuthYubikeyComponent, ], diff --git a/apps/web/src/app/auth/two-factor-auth.component.ts b/apps/web/src/app/auth/two-factor-auth.component.ts index 8bdd458ea99..9834529b52d 100644 --- a/apps/web/src/app/auth/two-factor-auth.component.ts +++ b/apps/web/src/app/auth/two-factor-auth.component.ts @@ -20,6 +20,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { LinkModule, TypographyModule, CheckboxModule, DialogService } from "@bitwarden/components"; import { TwoFactorAuthAuthenticatorComponent } from "../../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-auth-authenticator.component"; +import { TwoFactorAuthEmailComponent } from "../../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-auth-email.component"; import { TwoFactorAuthYubikeyComponent } from "../../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-auth-yubikey.component"; import { TwoFactorAuthComponent as BaseTwoFactorAuthComponent } from "../../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-auth.component"; import { TwoFactorOptionsComponent } from "../../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-options.component"; @@ -50,6 +51,7 @@ import { FormFieldModule } from "../../../../../libs/components/src/form-field"; RouterLink, CheckboxModule, TwoFactorOptionsComponent, + TwoFactorAuthEmailComponent, TwoFactorAuthAuthenticatorComponent, TwoFactorAuthYubikeyComponent, ], diff --git a/libs/angular/src/auth/components/two-factor-auth/two-factor-auth-email.component.html b/libs/angular/src/auth/components/two-factor-auth/two-factor-auth-email.component.html new file mode 100644 index 00000000000..c9d0901bcae --- /dev/null +++ b/libs/angular/src/auth/components/two-factor-auth/two-factor-auth-email.component.html @@ -0,0 +1,19 @@ +

+ {{ "enterVerificationCodeEmail" | i18n: twoFactorEmail }} +

+ + {{ "verificationCode" | i18n }} + + + + {{ "sendVerificationCodeEmailAgain" | i18n }} + + diff --git a/libs/angular/src/auth/components/two-factor-auth/two-factor-auth-email.component.ts b/libs/angular/src/auth/components/two-factor-auth/two-factor-auth-email.component.ts new file mode 100644 index 00000000000..7ac18bbc962 --- /dev/null +++ b/libs/angular/src/auth/components/two-factor-auth/two-factor-auth-email.component.ts @@ -0,0 +1,109 @@ +import { DialogModule } from "@angular/cdk/dialog"; +import { CommonModule } from "@angular/common"; +import { Component, EventEmitter, Output } from "@angular/core"; +import { ReactiveFormsModule, FormsModule } from "@angular/forms"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; +import { LoginStrategyServiceAbstraction } from "@bitwarden/auth/common"; +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; +import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; +import { TwoFactorEmailRequest } from "@bitwarden/common/auth/models/request/two-factor-email.request"; +import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.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"; +import { + ButtonModule, + LinkModule, + TypographyModule, + FormFieldModule, + AsyncActionsModule, +} from "@bitwarden/components"; + +@Component({ + standalone: true, + selector: "app-two-factor-auth-email", + templateUrl: "two-factor-auth-email.component.html", + imports: [ + CommonModule, + JslibModule, + DialogModule, + ButtonModule, + LinkModule, + TypographyModule, + ReactiveFormsModule, + FormFieldModule, + AsyncActionsModule, + FormsModule, + ], + providers: [I18nPipe], +}) +export class TwoFactorAuthEmailComponent { + @Output() token = new EventEmitter(); + + twoFactorEmail: string = null; + emailPromise: Promise; + tokenValue: string = ""; + + constructor( + protected i18nService: I18nService, + protected twoFactorService: TwoFactorService, + protected loginStrategyService: LoginStrategyServiceAbstraction, + protected platformUtilsService: PlatformUtilsService, + protected logService: LogService, + protected apiService: ApiService, + protected appIdService: AppIdService, + ) {} + + async ngOnInit(): Promise { + const providerData = await this.twoFactorService.getProviders().then((providers) => { + return providers.get(TwoFactorProviderType.Email); + }); + this.twoFactorEmail = providerData.Email; + + if ((await this.twoFactorService.getProviders()).size > 1) { + await this.sendEmail(false); + } + } + + async sendEmail(doToast: boolean) { + if (this.emailPromise != null) { + return; + } + + if ((await this.loginStrategyService.getEmail()) == null) { + this.platformUtilsService.showToast( + "error", + this.i18nService.t("errorOccurred"), + this.i18nService.t("sessionTimeout"), + ); + return; + } + + try { + const request = new TwoFactorEmailRequest(); + request.email = await this.loginStrategyService.getEmail(); + request.masterPasswordHash = await this.loginStrategyService.getMasterPasswordHash(); + request.ssoEmail2FaSessionToken = + await this.loginStrategyService.getSsoEmail2FaSessionToken(); + request.deviceIdentifier = await this.appIdService.getAppId(); + request.authRequestAccessCode = await this.loginStrategyService.getAccessCode(); + request.authRequestId = await this.loginStrategyService.getAuthRequestId(); + this.emailPromise = this.apiService.postTwoFactorEmail(request); + await this.emailPromise; + if (doToast) { + this.platformUtilsService.showToast( + "success", + null, + this.i18nService.t("verificationCodeEmailSent", this.twoFactorEmail), + ); + } + } catch (e) { + this.logService.error(e); + } + + this.emailPromise = null; + } +} diff --git a/libs/angular/src/auth/components/two-factor-auth/two-factor-auth.component.html b/libs/angular/src/auth/components/two-factor-auth/two-factor-auth.component.html index 1de1561a344..1d29cc5a4ff 100644 --- a/libs/angular/src/auth/components/two-factor-auth/two-factor-auth.component.html +++ b/libs/angular/src/auth/components/two-factor-auth/two-factor-auth.component.html @@ -1,5 +1,9 @@
+ Date: Mon, 15 Jul 2024 10:41:10 -0400 Subject: [PATCH 27/57] [PM-8979] Check that user is authed before getting user config (#10031) * Check that user is authed before getting user config * Accept PR Suggestion Co-authored-by: Andreas Coroiu * Use Strict Equal --------- Co-authored-by: Andreas Coroiu --- .../browser/src/background/main.background.ts | 1 + apps/cli/src/service-container.ts | 1 + .../src/services/jslib-services.module.ts | 8 ++++- .../services/config/config.service.spec.ts | 31 +++++++++++++++++++ .../services/config/default-config.service.ts | 21 ++++++++++--- 5 files changed, 56 insertions(+), 6 deletions(-) diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index d438bced4b3..9809f6a3408 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -716,6 +716,7 @@ export default class MainBackground { this.environmentService, this.logService, this.stateProvider, + this.authService, ); this.cipherService = new CipherService( diff --git a/apps/cli/src/service-container.ts b/apps/cli/src/service-container.ts index aeb233a31da..b352af7de7b 100644 --- a/apps/cli/src/service-container.ts +++ b/apps/cli/src/service-container.ts @@ -586,6 +586,7 @@ export class ServiceContainer { this.environmentService, this.logService, this.stateProvider, + this.authService, ); this.cipherService = new CipherService( diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 480acdd74d1..619155c9414 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -954,7 +954,13 @@ const safeProviders: SafeProvider[] = [ safeProvider({ provide: DefaultConfigService, useClass: DefaultConfigService, - deps: [ConfigApiServiceAbstraction, EnvironmentService, LogService, StateProvider], + deps: [ + ConfigApiServiceAbstraction, + EnvironmentService, + LogService, + StateProvider, + AuthServiceAbstraction, + ], }), safeProvider({ provide: ConfigService, diff --git a/libs/common/src/platform/services/config/config.service.spec.ts b/libs/common/src/platform/services/config/config.service.spec.ts index d643311a26f..d7e33473d01 100644 --- a/libs/common/src/platform/services/config/config.service.spec.ts +++ b/libs/common/src/platform/services/config/config.service.spec.ts @@ -14,6 +14,8 @@ import { mockAccountServiceWith, } from "../../../../spec"; import { subscribeTo } from "../../../../spec/observable-tracker"; +import { AuthService } from "../../../auth/abstractions/auth.service"; +import { AuthenticationStatus } from "../../../auth/enums/authentication-status"; import { UserId } from "../../../types/guid"; import { ConfigApiServiceAbstraction } from "../../abstractions/config/config-api.service.abstraction"; import { ServerConfig } from "../../abstractions/config/server-config"; @@ -39,6 +41,9 @@ describe("ConfigService", () => { const configApiService = mock(); const environmentService = mock(); const logService = mock(); + const authService = mock({ + authStatusFor$: (userId) => of(AuthenticationStatus.Unlocked), + }); let stateProvider: FakeStateProvider; let globalState: FakeGlobalState>; let userState: FakeSingleUserState; @@ -71,6 +76,7 @@ describe("ConfigService", () => { environmentService, logService, stateProvider, + authService, ); }); @@ -188,6 +194,30 @@ describe("ConfigService", () => { }); }); + it("gets global config when there is an locked active user", async () => { + await accountService.switchAccount(userId); + environmentService.environment$ = of(environmentFactory(activeApiUrl)); + + globalState.stateSubject.next({ + [activeApiUrl]: serverConfigFactory(activeApiUrl + "global"), + }); + userState.nextState(serverConfigFactory(userId)); + + const sut = new DefaultConfigService( + configApiService, + environmentService, + logService, + stateProvider, + mock({ + authStatusFor$: () => of(AuthenticationStatus.Locked), + }), + ); + + const config = await firstValueFrom(sut.serverConfig$); + + expect(config.gitHash).toEqual(activeApiUrl + "global"); + }); + describe("environment change", () => { let sut: DefaultConfigService; let environmentSubject: Subject; @@ -205,6 +235,7 @@ describe("ConfigService", () => { environmentService, logService, stateProvider, + authService, ); }); diff --git a/libs/common/src/platform/services/config/default-config.service.ts b/libs/common/src/platform/services/config/default-config.service.ts index 0a306348d7b..16878a72832 100644 --- a/libs/common/src/platform/services/config/default-config.service.ts +++ b/libs/common/src/platform/services/config/default-config.service.ts @@ -13,6 +13,8 @@ import { } from "rxjs"; import { SemVer } from "semver"; +import { AuthService } from "../../../auth/abstractions/auth.service"; +import { AuthenticationStatus } from "../../../auth/enums/authentication-status"; import { DefaultFeatureFlagValue, FeatureFlag, @@ -60,16 +62,25 @@ export class DefaultConfigService implements ConfigService { private environmentService: EnvironmentService, private logService: LogService, private stateProvider: StateProvider, + private authService: AuthService, ) { const apiUrl$ = this.environmentService.environment$.pipe( map((environment) => environment.getApiUrl()), ); + const userId$ = this.stateProvider.activeUserId$; + const authStatus$ = userId$.pipe( + switchMap((userId) => (userId == null ? of(null) : this.authService.authStatusFor$(userId))), + ); - this.serverConfig$ = combineLatest([this.stateProvider.activeUserId$, apiUrl$]).pipe( - switchMap(([userId, apiUrl]) => { - const config$ = - userId == null ? this.globalConfigFor$(apiUrl) : this.userConfigFor$(userId); - return config$.pipe(map((config) => [config, userId, apiUrl] as const)); + this.serverConfig$ = combineLatest([userId$, apiUrl$, authStatus$]).pipe( + switchMap(([userId, apiUrl, authStatus]) => { + if (userId == null || authStatus !== AuthenticationStatus.Unlocked) { + return this.globalConfigFor$(apiUrl).pipe( + map((config) => [config, null, apiUrl] as const), + ); + } + + return this.userConfigFor$(userId).pipe(map((config) => [config, userId, apiUrl] as const)); }), tap(async (rec) => { const [existingConfig, userId, apiUrl] = rec; From 974162b1a4a6a0013a95ed1046d0157b3736fe3d Mon Sep 17 00:00:00 2001 From: Cesar Gonzalez Date: Mon, 15 Jul 2024 10:32:30 -0500 Subject: [PATCH 28/57] [PM-6426] Create TaskSchedulerService and update long lived timeouts in the extension to leverage the new service (#8566) * [PM-6426] Create TaskSchedulerService and update usage of long lived timeouts * [PM-6426] Implementing nextSync timeout using TaskScheduler * [PM-6426] Implementing systemClearClipboard using TaskScheduler * [PM-6426] Fixing race condition with setting/unsetting active alarms * [PM-6426] Implementing clear clipboard call on generatePasswordToClipboard with the TaskSchedulerService * [PM-6426] Implementing abortTimeout for Fido2ClientService using TaskSchedulerService * [PM-6426] Implementing reconnect timer timeout for NotificationService using the TaskSchedulerService * [PM-6426] Implementing reconnect timer timeout for NotificationService using the TaskSchedulerService * [PM-6426] Implementing sessionTimeout for LoginStrategyService using TaskSchedulerService * [PM-6426] Implementing eventUploadInterval using TaskScheduler * [PM-6426] Adding jest tests for the base TaskSchedulerService class * [PM-6426] Updating jest tests for GeneratePasswordToClipboardCommand * [PM-6426] Setting up the full sync process as an interval rather than a timeout * [PM-6426] Renaming the scheduleNextSync alarm name * [PM-6426] Fixing dependency references in services.module.ts * [PM-6426] Adding jest tests for added BrowserApi methods * [PM-6426] Refactoring small detail for how we identify the clear clipboard timeout in SystemService * [PM-6426] Ensuring that we await clearing an established scheduled task for the notification service * [PM-6426] Changing the name of the state definition for the TaskScheduler * [PM-6426] Implementing jest tests for the BrowserTaskSchedulerService * [PM-6426] Implementing jest tests for the BrowserTaskSchedulerService * [PM-6426] Adding jest tests for the base TaskSchedulerService class * [PM-6426] Finalizing jest tests for BrowserTaskScheduler class * [PM-6426] Finalizing documentation on BrowserTaskSchedulerService * [PM-6426] Fixing jest test for LoginStrategyService * [PM-6426] Implementing compatibility for the browser.alarms api * [PM-6426] Fixing how we check for the browser alarms api * [PM-6426] Adding jest tests to the BrowserApi implementation * [PM-6426] Aligning the implementation with our code guidelines for Angular components * [PM-6426] Fixing jest tests and lint errors * [PM-6426] Moving alarms api calls out of BrowserApi and structuring them within the BrowserTaskSchedulerService * [PM-6426] Reworking implementation to register handlers separately from the call to those handlers * [PM-6426] Adjusting how we register the fullSync scheduled task * [PM-6426] Implementing approach for incorporating the user UUID when setting task handlers * [PM-6426] Attempting to re-work implementation to facilitate userId-spcific alarms * [PM-6426] Refactoring smaller details of the implementation * [PM-6426] Working through the details of the implementation and setting up final refinments * [PM-6426] Fixing some issues surrounding duplicate alarms triggering * [PM-6426] Adjusting name for generate password to clipboard command task name * [PM-6426] Fixing generate password to clipboard command jest tests * [PM-6426] Working through jest tests and implementing a method to guard against setting a task without having a registered callback * [PM-6426] Working through jest tests and implementing a method to guard against setting a task without having a registered callback * [PM-6426] Implementing methodology for having a fallback to setTimeout if the browser context is lost in some manner * [PM-6426] Working through jest tests * [PM-6426] Working through jest tests * [PM-6426] Working through jest tests * [PM-6426] Working through jest tests * [PM-6426] Finalizing stepped setInterval implementation * [PM-6426] Implementing Jest tests for DefaultTaskSchedulerService * [PM-6426] Adjusting jest tests * [PM-6426] Adjusting jest tests * [PM-6426] Adjusting jest tests * [PM-6426] Fixing issues identified in code review * [PM-6426] Fixing issues identified in code review * [PM-6426] Removing user-based alarms and fixing an issue found with setting steppedd alarm interavals * [PM-6426] Removing user-based alarms and fixing an issue found with setting steppedd alarm interavals * [PM-6426] Fixing issue with typing information on a test * [PM-6426] Using the getUpperBoundDelayInMinutes method to handle setting stepped alarms and setTimeout fallbacks * [PM-6426] Removing the potential for the TaskScheduler to be optional * [PM-6426] Reworking implementation to leverage subscription based deregistration of alarms * [PM-6426] Fixing jest tests * [PM-6426] Implementing foreground and background task scheduler services to avoid duplication of task scheudlers and to have the background setup as a fallback to the poopup tasks * [PM-6426] Implementing foreground and background task scheduler services to avoid duplication of task scheudlers and to have the background setup as a fallback to the poopup tasks * [PM-6426] Merging main into branch * [PM-6426] Fixing issues with the CLI Service Container implementation * [PM-6426] Reworking swallowed promises to contain a catch statement allow us to debug potential issues with registrations of alarms * [PM-6426] Adding jest tests to the ForegroundTaskSchedulerService and the BackgroundTaskSchedulerService * [PM-6426] Adding jest tests to the ForegroundTaskSchedulerService and the BackgroundTaskSchedulerService * [PM-6426] Adding jest tests to the ForegroundTaskSchedulerService and the BackgroundTaskSchedulerService * [PM-6426] Adding jest tests to the ForegroundTaskSchedulerService and the BackgroundTaskSchedulerService * [PM-6426] Adjusting implementation based on code review feedback * [PM-6426] Reworking file structure * [PM-6426] Reworking file structure * [PM-6426] Adding comments to provide clarity on how the login strategy cache experiation state is used * [PM-6426] Catching and logging erorrs that appear from methods that return a promise within VaultTimeoutService --- .../src/autofill/clipboard/clear-clipboard.ts | 4 +- ...rate-password-to-clipboard-command.spec.ts | 47 +- .../generate-password-to-clipboard-command.ts | 35 +- .../src/autofill/spec/testing-utils.ts | 49 +- .../browser/src/background/main.background.ts | 39 +- apps/browser/src/manifest.json | 1 + apps/browser/src/manifest.v3.json | 1 + .../src/platform/alarms/alarm-state.ts | 66 --- .../src/platform/alarms/on-alarm-listener.ts | 28 -- .../src/platform/alarms/register-alarms.ts | 31 -- .../platform/listeners/on-command-listener.ts | 0 .../browser-task-scheduler.service.ts | 33 ++ .../background-task-scheduler.service.spec.ts | 129 +++++ .../background-task-scheduler.service.ts | 75 +++ .../browser-task-scheduler.service.spec.ts | 463 ++++++++++++++++++ .../browser-task-scheduler.service.ts | 427 ++++++++++++++++ .../foreground-task-scheduler.service.spec.ts | 79 +++ .../foreground-task-scheduler.service.ts | 71 +++ .../src/popup/services/services.module.ts | 11 + .../vault-timeout/vault-timeout.service.ts | 13 +- apps/browser/test.setup.ts | 13 + apps/cli/src/service-container.ts | 10 + .../src/app/services/services.module.ts | 2 + .../src/services/jslib-services.module.ts | 21 +- .../login-strategy.service.spec.ts | 4 + .../login-strategy.service.ts | 24 +- .../default-task-scheduler.service.spec.ts | 123 +++++ .../default-task-scheduler.service.ts | 97 ++++ libs/common/src/platform/scheduling/index.ts | 3 + .../scheduling/scheduled-task-name.enum.ts | 12 + .../scheduling/task-scheduler.service.ts | 16 + .../fido2/fido2-client.service.spec.ts | 6 +- .../services/fido2/fido2-client.service.ts | 91 ++-- .../src/platform/services/system.service.ts | 42 +- .../src/platform/state/state-definitions.ts | 1 + .../services/event/event-upload.service.ts | 18 +- .../src/services/notifications.service.ts | 24 +- .../vault-timeout.service.spec.ts | 8 + .../vault-timeout/vault-timeout.service.ts | 20 +- 39 files changed, 1854 insertions(+), 283 deletions(-) delete mode 100644 apps/browser/src/platform/alarms/alarm-state.ts delete mode 100644 apps/browser/src/platform/alarms/on-alarm-listener.ts delete mode 100644 apps/browser/src/platform/alarms/register-alarms.ts create mode 100644 apps/browser/src/platform/listeners/on-command-listener.ts create mode 100644 apps/browser/src/platform/services/abstractions/browser-task-scheduler.service.ts create mode 100644 apps/browser/src/platform/services/task-scheduler/background-task-scheduler.service.spec.ts create mode 100644 apps/browser/src/platform/services/task-scheduler/background-task-scheduler.service.ts create mode 100644 apps/browser/src/platform/services/task-scheduler/browser-task-scheduler.service.spec.ts create mode 100644 apps/browser/src/platform/services/task-scheduler/browser-task-scheduler.service.ts create mode 100644 apps/browser/src/platform/services/task-scheduler/foreground-task-scheduler.service.spec.ts create mode 100644 apps/browser/src/platform/services/task-scheduler/foreground-task-scheduler.service.ts create mode 100644 libs/common/src/platform/scheduling/default-task-scheduler.service.spec.ts create mode 100644 libs/common/src/platform/scheduling/default-task-scheduler.service.ts create mode 100644 libs/common/src/platform/scheduling/index.ts create mode 100644 libs/common/src/platform/scheduling/scheduled-task-name.enum.ts create mode 100644 libs/common/src/platform/scheduling/task-scheduler.service.ts diff --git a/apps/browser/src/autofill/clipboard/clear-clipboard.ts b/apps/browser/src/autofill/clipboard/clear-clipboard.ts index f8018bb036a..426d6539513 100644 --- a/apps/browser/src/autofill/clipboard/clear-clipboard.ts +++ b/apps/browser/src/autofill/clipboard/clear-clipboard.ts @@ -1,11 +1,9 @@ import { BrowserApi } from "../../platform/browser/browser-api"; -export const clearClipboardAlarmName = "clearClipboard"; - export class ClearClipboard { /** We currently rely on an active tab with an injected content script (`../content/misc-utils.ts`) to clear the clipboard via `window.navigator.clipboard.writeText(text)` - + With https://bugs.chromium.org/p/chromium/issues/detail?id=1160302 it was said that service workers, would have access to the clipboard api and then we could migrate to a simpler solution */ diff --git a/apps/browser/src/autofill/clipboard/generate-password-to-clipboard-command.spec.ts b/apps/browser/src/autofill/clipboard/generate-password-to-clipboard-command.spec.ts index 522da229244..d0d42cc06f7 100644 --- a/apps/browser/src/autofill/clipboard/generate-password-to-clipboard-command.spec.ts +++ b/apps/browser/src/autofill/clipboard/generate-password-to-clipboard-command.spec.ts @@ -1,30 +1,45 @@ import { mock, MockProxy } from "jest-mock-extended"; +import { firstValueFrom, Subscription } from "rxjs"; import { AutofillSettingsService } from "@bitwarden/common/autofill/services/autofill-settings.service"; +import { ScheduledTaskNames } from "@bitwarden/common/platform/scheduling"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; -import { setAlarmTime } from "../../platform/alarms/alarm-state"; import { BrowserApi } from "../../platform/browser/browser-api"; +import { BrowserTaskSchedulerService } from "../../platform/services/abstractions/browser-task-scheduler.service"; -import { clearClipboardAlarmName } from "./clear-clipboard"; +import { ClearClipboard } from "./clear-clipboard"; import { GeneratePasswordToClipboardCommand } from "./generate-password-to-clipboard-command"; -jest.mock("../../platform/alarms/alarm-state", () => { +jest.mock("rxjs", () => { + const actual = jest.requireActual("rxjs"); return { - setAlarmTime: jest.fn(), + ...actual, + firstValueFrom: jest.fn(), }; }); -const setAlarmTimeMock = setAlarmTime as jest.Mock; - describe("GeneratePasswordToClipboardCommand", () => { let passwordGenerationService: MockProxy; let autofillSettingsService: MockProxy; + let browserTaskSchedulerService: MockProxy; let sut: GeneratePasswordToClipboardCommand; beforeEach(() => { passwordGenerationService = mock(); + autofillSettingsService = mock(); + browserTaskSchedulerService = mock({ + setTimeout: jest.fn((taskName, timeoutInMs) => { + const timeoutHandle = setTimeout(() => { + if (taskName === ScheduledTaskNames.generatePasswordClearClipboardTimeout) { + void ClearClipboard.run(); + } + }, timeoutInMs); + + return new Subscription(() => clearTimeout(timeoutHandle)); + }), + }); passwordGenerationService.getOptions.mockResolvedValue([{ length: 8 }, {} as any]); @@ -35,6 +50,7 @@ describe("GeneratePasswordToClipboardCommand", () => { sut = new GeneratePasswordToClipboardCommand( passwordGenerationService, autofillSettingsService, + browserTaskSchedulerService, ); }); @@ -44,20 +60,24 @@ describe("GeneratePasswordToClipboardCommand", () => { describe("generatePasswordToClipboard", () => { it("has clear clipboard value", async () => { - jest.spyOn(sut as any, "getClearClipboard").mockImplementation(() => 5 * 60); // 5 minutes + jest.useFakeTimers(); + jest.spyOn(ClearClipboard, "run"); + (firstValueFrom as jest.Mock).mockResolvedValue(2 * 60); // 2 minutes await sut.generatePasswordToClipboard({ id: 1 } as any); + jest.advanceTimersByTime(2 * 60 * 1000); expect(jest.spyOn(BrowserApi, "sendTabsMessage")).toHaveBeenCalledTimes(1); - expect(jest.spyOn(BrowserApi, "sendTabsMessage")).toHaveBeenCalledWith(1, { command: "copyText", text: "PASSWORD", }); - - expect(setAlarmTimeMock).toHaveBeenCalledTimes(1); - - expect(setAlarmTimeMock).toHaveBeenCalledWith(clearClipboardAlarmName, expect.any(Number)); + expect(browserTaskSchedulerService.setTimeout).toHaveBeenCalledTimes(1); + expect(browserTaskSchedulerService.setTimeout).toHaveBeenCalledWith( + ScheduledTaskNames.generatePasswordClearClipboardTimeout, + expect.any(Number), + ); + expect(ClearClipboard.run).toHaveBeenCalledTimes(1); }); it("does not have clear clipboard value", async () => { @@ -71,8 +91,7 @@ describe("GeneratePasswordToClipboardCommand", () => { command: "copyText", text: "PASSWORD", }); - - expect(setAlarmTimeMock).not.toHaveBeenCalled(); + expect(browserTaskSchedulerService.setTimeout).not.toHaveBeenCalled(); }); }); }); diff --git a/apps/browser/src/autofill/clipboard/generate-password-to-clipboard-command.ts b/apps/browser/src/autofill/clipboard/generate-password-to-clipboard-command.ts index dadd61fbd12..cf3bc311aea 100644 --- a/apps/browser/src/autofill/clipboard/generate-password-to-clipboard-command.ts +++ b/apps/browser/src/autofill/clipboard/generate-password-to-clipboard-command.ts @@ -1,18 +1,25 @@ -import { firstValueFrom } from "rxjs"; +import { firstValueFrom, Subscription } from "rxjs"; import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; +import { TaskSchedulerService, ScheduledTaskNames } from "@bitwarden/common/platform/scheduling"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; -import { setAlarmTime } from "../../platform/alarms/alarm-state"; - -import { clearClipboardAlarmName } from "./clear-clipboard"; +import { ClearClipboard } from "./clear-clipboard"; import { copyToClipboard } from "./copy-to-clipboard-command"; export class GeneratePasswordToClipboardCommand { + private clearClipboardSubscription: Subscription; + constructor( private passwordGenerationService: PasswordGenerationServiceAbstraction, private autofillSettingsService: AutofillSettingsServiceAbstraction, - ) {} + private taskSchedulerService: TaskSchedulerService, + ) { + this.taskSchedulerService.registerTaskHandler( + ScheduledTaskNames.generatePasswordClearClipboardTimeout, + () => ClearClipboard.run(), + ); + } async getClearClipboard() { return await firstValueFrom(this.autofillSettingsService.clearClipboardDelay$); @@ -22,14 +29,18 @@ export class GeneratePasswordToClipboardCommand { const [options] = await this.passwordGenerationService.getOptions(); const password = await this.passwordGenerationService.generatePassword(options); - // 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 - copyToClipboard(tab, password); + await copyToClipboard(tab, password); - const clearClipboard = await this.getClearClipboard(); - - if (clearClipboard != null) { - await setAlarmTime(clearClipboardAlarmName, clearClipboard * 1000); + const clearClipboardDelayInSeconds = await this.getClearClipboard(); + if (!clearClipboardDelayInSeconds) { + return; } + + const timeoutInMs = clearClipboardDelayInSeconds * 1000; + this.clearClipboardSubscription?.unsubscribe(); + this.clearClipboardSubscription = this.taskSchedulerService.setTimeout( + ScheduledTaskNames.generatePasswordClearClipboardTimeout, + timeoutInMs, + ); } } diff --git a/apps/browser/src/autofill/spec/testing-utils.ts b/apps/browser/src/autofill/spec/testing-utils.ts index 5b0db5ebd6f..ba7a5844987 100644 --- a/apps/browser/src/autofill/spec/testing-utils.ts +++ b/apps/browser/src/autofill/spec/testing-utils.ts @@ -1,21 +1,21 @@ import { mock } from "jest-mock-extended"; -function triggerTestFailure() { +export function triggerTestFailure() { expect(true).toBe("Test has failed."); } const scheduler = typeof setImmediate === "function" ? setImmediate : setTimeout; -function flushPromises() { +export function flushPromises() { return new Promise(function (resolve) { scheduler(resolve); }); } -function postWindowMessage(data: any, origin = "https://localhost/", source = window) { +export function postWindowMessage(data: any, origin = "https://localhost/", source = window) { globalThis.dispatchEvent(new MessageEvent("message", { data, origin, source })); } -function sendMockExtensionMessage( +export function sendMockExtensionMessage( message: any, sender?: chrome.runtime.MessageSender, sendResponse?: CallableFunction, @@ -32,7 +32,7 @@ function sendMockExtensionMessage( ); } -function triggerRuntimeOnConnectEvent(port: chrome.runtime.Port) { +export function triggerRuntimeOnConnectEvent(port: chrome.runtime.Port) { (chrome.runtime.onConnect.addListener as unknown as jest.SpyInstance).mock.calls.forEach( (call) => { const callback = call[0]; @@ -41,21 +41,21 @@ function triggerRuntimeOnConnectEvent(port: chrome.runtime.Port) { ); } -function sendPortMessage(port: chrome.runtime.Port, message: any) { +export function sendPortMessage(port: chrome.runtime.Port, message: any) { (port.onMessage.addListener as unknown as jest.SpyInstance).mock.calls.forEach((call) => { const callback = call[0]; callback(message || {}, port); }); } -function triggerPortOnDisconnectEvent(port: chrome.runtime.Port) { +export function triggerPortOnDisconnectEvent(port: chrome.runtime.Port) { (port.onDisconnect.addListener as unknown as jest.SpyInstance).mock.calls.forEach((call) => { const callback = call[0]; callback(port); }); } -function triggerWindowOnFocusedChangedEvent(windowId: number) { +export function triggerWindowOnFocusedChangedEvent(windowId: number) { (chrome.windows.onFocusChanged.addListener as unknown as jest.SpyInstance).mock.calls.forEach( (call) => { const callback = call[0]; @@ -64,7 +64,7 @@ function triggerWindowOnFocusedChangedEvent(windowId: number) { ); } -function triggerTabOnActivatedEvent(activeInfo: chrome.tabs.TabActiveInfo) { +export function triggerTabOnActivatedEvent(activeInfo: chrome.tabs.TabActiveInfo) { (chrome.tabs.onActivated.addListener as unknown as jest.SpyInstance).mock.calls.forEach( (call) => { const callback = call[0]; @@ -73,14 +73,14 @@ function triggerTabOnActivatedEvent(activeInfo: chrome.tabs.TabActiveInfo) { ); } -function triggerTabOnReplacedEvent(addedTabId: number, removedTabId: number) { +export function triggerTabOnReplacedEvent(addedTabId: number, removedTabId: number) { (chrome.tabs.onReplaced.addListener as unknown as jest.SpyInstance).mock.calls.forEach((call) => { const callback = call[0]; callback(addedTabId, removedTabId); }); } -function triggerTabOnUpdatedEvent( +export function triggerTabOnUpdatedEvent( tabId: number, changeInfo: chrome.tabs.TabChangeInfo, tab: chrome.tabs.Tab, @@ -91,14 +91,21 @@ function triggerTabOnUpdatedEvent( }); } -function triggerTabOnRemovedEvent(tabId: number, removeInfo: chrome.tabs.TabRemoveInfo) { +export function triggerTabOnRemovedEvent(tabId: number, removeInfo: chrome.tabs.TabRemoveInfo) { (chrome.tabs.onRemoved.addListener as unknown as jest.SpyInstance).mock.calls.forEach((call) => { const callback = call[0]; callback(tabId, removeInfo); }); } -function mockQuerySelectorAllDefinedCall() { +export function triggerOnAlarmEvent(alarm: chrome.alarms.Alarm) { + (chrome.alarms.onAlarm.addListener as unknown as jest.SpyInstance).mock.calls.forEach((call) => { + const callback = call[0]; + callback(alarm); + }); +} + +export function mockQuerySelectorAllDefinedCall() { const originalDocumentQuerySelectorAll = document.querySelectorAll; document.querySelectorAll = function (selector: string) { return originalDocumentQuerySelectorAll.call( @@ -125,19 +132,3 @@ function mockQuerySelectorAllDefinedCall() { }, }; } - -export { - triggerTestFailure, - flushPromises, - postWindowMessage, - sendMockExtensionMessage, - triggerRuntimeOnConnectEvent, - sendPortMessage, - triggerPortOnDisconnectEvent, - triggerWindowOnFocusedChangedEvent, - triggerTabOnActivatedEvent, - triggerTabOnReplacedEvent, - triggerTabOnUpdatedEvent, - triggerTabOnRemovedEvent, - mockQuerySelectorAllDefinedCall, -}; diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 9809f6a3408..35e674cfd1c 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -105,6 +105,7 @@ import { clearCaches } from "@bitwarden/common/platform/misc/sequentialize"; import { Account } from "@bitwarden/common/platform/models/domain/account"; import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { ScheduledTaskNames } from "@bitwarden/common/platform/scheduling"; import { AppIdService } from "@bitwarden/common/platform/services/app-id.service"; import { ConfigApiService } from "@bitwarden/common/platform/services/config/config-api.service"; import { DefaultConfigService } from "@bitwarden/common/platform/services/config/default-config.service"; @@ -216,6 +217,7 @@ import { ChromeMessageSender } from "../platform/messaging/chrome-message.sender /* eslint-enable no-restricted-imports */ import { OffscreenDocumentService } from "../platform/offscreen-document/abstractions/offscreen-document"; import { DefaultOffscreenDocumentService } from "../platform/offscreen-document/offscreen-document.service"; +import { BrowserTaskSchedulerService } from "../platform/services/abstractions/browser-task-scheduler.service"; import { BrowserCryptoService } from "../platform/services/browser-crypto.service"; import { BrowserEnvironmentService } from "../platform/services/browser-environment.service"; import BrowserLocalStorageService from "../platform/services/browser-local-storage.service"; @@ -225,6 +227,8 @@ import I18nService from "../platform/services/i18n.service"; import { LocalBackedSessionStorageService } from "../platform/services/local-backed-session-storage.service"; import { BackgroundPlatformUtilsService } from "../platform/services/platform-utils/background-platform-utils.service"; import { BrowserPlatformUtilsService } from "../platform/services/platform-utils/browser-platform-utils.service"; +import { BackgroundTaskSchedulerService } from "../platform/services/task-scheduler/background-task-scheduler.service"; +import { ForegroundTaskSchedulerService } from "../platform/services/task-scheduler/foreground-task-scheduler.service"; import { BackgroundMemoryStorageService } from "../platform/storage/background-memory-storage.service"; import { BrowserStorageServiceProvider } from "../platform/storage/browser-storage-service.provider"; import { ForegroundMemoryStorageService } from "../platform/storage/foreground-memory-storage.service"; @@ -322,6 +326,7 @@ export default class MainBackground { activeUserStateProvider: ActiveUserStateProvider; derivedStateProvider: DerivedStateProvider; stateProvider: StateProvider; + taskSchedulerService: BrowserTaskSchedulerService; fido2Background: Fido2BackgroundAbstraction; individualVaultExportService: IndividualVaultExportServiceAbstraction; organizationVaultExportService: OrganizationVaultExportServiceAbstraction; @@ -511,6 +516,14 @@ export default class MainBackground { this.globalStateProvider, this.derivedStateProvider, ); + + this.taskSchedulerService = this.popupOnlyContext + ? new ForegroundTaskSchedulerService(this.logService, this.stateProvider) + : new BackgroundTaskSchedulerService(this.logService, this.stateProvider); + this.taskSchedulerService.registerTaskHandler(ScheduledTaskNames.scheduleNextSyncInterval, () => + this.fullSync(), + ); + this.environmentService = new BrowserEnvironmentService( this.logService, this.stateProvider, @@ -779,6 +792,8 @@ export default class MainBackground { this.authService, this.vaultTimeoutSettingsService, this.stateEventRunnerService, + this.taskSchedulerService, + this.logService, lockedCallback, logoutCallback, ); @@ -858,6 +873,7 @@ export default class MainBackground { this.stateProvider, this.logService, this.authService, + this.taskSchedulerService, ); this.eventCollectionService = new EventCollectionService( this.cipherService, @@ -935,6 +951,7 @@ export default class MainBackground { this.stateService, this.authService, this.messagingService, + this.taskSchedulerService, ); this.fido2UserInterfaceService = new BrowserFido2UserInterfaceService(this.authService); @@ -950,16 +967,17 @@ export default class MainBackground { this.authService, this.vaultSettingsService, this.domainSettingsService, + this.taskSchedulerService, this.logService, ); - const systemUtilsServiceReloadCallback = () => { + const systemUtilsServiceReloadCallback = async () => { const forceWindowReload = this.platformUtilsService.isSafari() || this.platformUtilsService.isFirefox() || this.platformUtilsService.isOpera(); + await this.taskSchedulerService.clearAllScheduledTasks(); BrowserApi.reloadExtension(forceWindowReload ? self : null); - return Promise.resolve(); }; this.systemService = new SystemService( @@ -971,6 +989,7 @@ export default class MainBackground { this.vaultTimeoutSettingsService, this.biometricStateService, this.accountService, + this.taskSchedulerService, ); // Other fields @@ -1184,7 +1203,12 @@ export default class MainBackground { setTimeout(async () => { await this.refreshBadge(); await this.fullSync(true); + await this.taskSchedulerService.setInterval( + ScheduledTaskNames.scheduleNextSyncInterval, + 5 * 60 * 1000, // check every 5 minutes + ); setTimeout(() => this.notificationsService.init(), 2500); + await this.taskSchedulerService.verifyAlarmsState(); resolve(); }, 500); }); @@ -1453,17 +1477,6 @@ export default class MainBackground { if (override || lastSyncAgo >= syncInternal) { await this.syncService.fullSync(override); - this.scheduleNextSync(); - } else { - this.scheduleNextSync(); } } - - private scheduleNextSync() { - if (this.syncTimeout) { - clearTimeout(this.syncTimeout); - } - - this.syncTimeout = setTimeout(async () => await this.fullSync(), 5 * 60 * 1000); // check every 5 minutes - } } diff --git a/apps/browser/src/manifest.json b/apps/browser/src/manifest.json index b1c51911ec8..b9ab9e0dd9d 100644 --- a/apps/browser/src/manifest.json +++ b/apps/browser/src/manifest.json @@ -59,6 +59,7 @@ "clipboardRead", "clipboardWrite", "idle", + "alarms", "webRequest", "webRequestBlocking", "webNavigation" diff --git a/apps/browser/src/manifest.v3.json b/apps/browser/src/manifest.v3.json index 40060a7fd93..b9eac49764d 100644 --- a/apps/browser/src/manifest.v3.json +++ b/apps/browser/src/manifest.v3.json @@ -59,6 +59,7 @@ "clipboardRead", "clipboardWrite", "idle", + "alarms", "scripting", "offscreen", "webRequest", diff --git a/apps/browser/src/platform/alarms/alarm-state.ts b/apps/browser/src/platform/alarms/alarm-state.ts deleted file mode 100644 index fa18e26ed1c..00000000000 --- a/apps/browser/src/platform/alarms/alarm-state.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { clearClipboardAlarmName } from "../../autofill/clipboard"; -import { BrowserApi } from "../browser/browser-api"; - -export const alarmKeys = [clearClipboardAlarmName] as const; -export type AlarmKeys = (typeof alarmKeys)[number]; - -type AlarmState = { [T in AlarmKeys]: number | undefined }; - -const alarmState: AlarmState = { - clearClipboard: null, - //TODO once implemented vaultTimeout: null; - //TODO once implemented checkNotifications: null; - //TODO once implemented (if necessary) processReload: null; -}; - -/** - * Retrieves the set alarm time (planned execution) for a give an commandName {@link AlarmState} - * @param commandName A command that has been previously registered with {@link AlarmState} - * @returns {Promise} null or Unix epoch timestamp when the alarm action is supposed to execute - * @example - * // getAlarmTime(clearClipboard) - */ -export async function getAlarmTime(commandName: AlarmKeys): Promise { - let alarmTime: number; - if (BrowserApi.isManifestVersion(3)) { - const fromSessionStore = await chrome.storage.session.get(commandName); - alarmTime = fromSessionStore[commandName]; - } else { - alarmTime = alarmState[commandName]; - } - - return alarmTime; -} - -/** - * Registers an action that should execute after the given time has passed - * @param commandName A command that has been previously registered with {@link AlarmState} - * @param delay_ms The number of ms from now in which the command should execute from - * @example - * // setAlarmTime(clearClipboard, 5000) register the clearClipboard action which will execute when at least 5 seconds from now have passed - */ -export async function setAlarmTime(commandName: AlarmKeys, delay_ms: number): Promise { - if (!delay_ms || delay_ms === 0) { - await this.clearAlarmTime(commandName); - return; - } - - const time = Date.now() + delay_ms; - await setAlarmTimeInternal(commandName, time); -} - -/** - * Clears the time currently set for a given command - * @param commandName A command that has been previously registered with {@link AlarmState} - */ -export async function clearAlarmTime(commandName: AlarmKeys): Promise { - await setAlarmTimeInternal(commandName, null); -} - -async function setAlarmTimeInternal(commandName: AlarmKeys, time: number): Promise { - if (BrowserApi.isManifestVersion(3)) { - await chrome.storage.session.set({ [commandName]: time }); - } else { - alarmState[commandName] = time; - } -} diff --git a/apps/browser/src/platform/alarms/on-alarm-listener.ts b/apps/browser/src/platform/alarms/on-alarm-listener.ts deleted file mode 100644 index 274f19f7897..00000000000 --- a/apps/browser/src/platform/alarms/on-alarm-listener.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { ClearClipboard, clearClipboardAlarmName } from "../../autofill/clipboard"; - -import { alarmKeys, clearAlarmTime, getAlarmTime } from "./alarm-state"; - -export const onAlarmListener = async (alarm: chrome.alarms.Alarm) => { - alarmKeys.forEach(async (key) => { - const executionTime = await getAlarmTime(key); - if (!executionTime) { - return; - } - - const currentDate = Date.now(); - if (executionTime > currentDate) { - return; - } - - await clearAlarmTime(key); - - switch (key) { - case clearClipboardAlarmName: - // 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 - ClearClipboard.run(); - break; - default: - } - }); -}; diff --git a/apps/browser/src/platform/alarms/register-alarms.ts b/apps/browser/src/platform/alarms/register-alarms.ts deleted file mode 100644 index 86b9fb97747..00000000000 --- a/apps/browser/src/platform/alarms/register-alarms.ts +++ /dev/null @@ -1,31 +0,0 @@ -const NUMBER_OF_ALARMS = 6; - -export function registerAlarms() { - alarmsToBeCreated(NUMBER_OF_ALARMS); -} - -/** - * Creates staggered alarms that periodically (1min) raise OnAlarm events. The staggering is calculated based on the number of alarms passed in. - * @param numberOfAlarms Number of named alarms, that shall be registered - * @example - * // alarmsToBeCreated(2) results in 2 alarms separated by 30 seconds - * @example - * // alarmsToBeCreated(4) results in 4 alarms separated by 15 seconds - * @example - * // alarmsToBeCreated(6) results in 6 alarms separated by 10 seconds - * @example - * // alarmsToBeCreated(60) results in 60 alarms separated by 1 second - */ -function alarmsToBeCreated(numberOfAlarms: number): void { - const oneMinuteInMs = 60 * 1000; - const offset = oneMinuteInMs / numberOfAlarms; - - let calculatedWhen: number = Date.now() + offset; - - for (let index = 0; index < numberOfAlarms; index++) { - // 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 - chrome.alarms.create(`bw_alarm${index}`, { periodInMinutes: 1, when: calculatedWhen }); - calculatedWhen += offset; - } -} diff --git a/apps/browser/src/platform/listeners/on-command-listener.ts b/apps/browser/src/platform/listeners/on-command-listener.ts new file mode 100644 index 00000000000..e69de29bb2d diff --git a/apps/browser/src/platform/services/abstractions/browser-task-scheduler.service.ts b/apps/browser/src/platform/services/abstractions/browser-task-scheduler.service.ts new file mode 100644 index 00000000000..58c4eb48897 --- /dev/null +++ b/apps/browser/src/platform/services/abstractions/browser-task-scheduler.service.ts @@ -0,0 +1,33 @@ +import { Observable } from "rxjs"; + +import { TaskSchedulerService, ScheduledTaskName } from "@bitwarden/common/platform/scheduling"; + +export const BrowserTaskSchedulerPortName = "browser-task-scheduler-port"; + +export const BrowserTaskSchedulerPortActions = { + setTimeout: "setTimeout", + setInterval: "setInterval", + clearAlarm: "clearAlarm", +} as const; +export type BrowserTaskSchedulerPortAction = keyof typeof BrowserTaskSchedulerPortActions; + +export type BrowserTaskSchedulerPortMessage = { + action: BrowserTaskSchedulerPortAction; + taskName: ScheduledTaskName; + alarmName?: string; + delayInMs?: number; + intervalInMs?: number; +}; + +export type ActiveAlarm = { + alarmName: string; + startTime: number; + createInfo: chrome.alarms.AlarmCreateInfo; +}; + +export abstract class BrowserTaskSchedulerService extends TaskSchedulerService { + activeAlarms$: Observable; + abstract clearAllScheduledTasks(): Promise; + abstract verifyAlarmsState(): Promise; + abstract clearScheduledAlarm(alarmName: string): Promise; +} diff --git a/apps/browser/src/platform/services/task-scheduler/background-task-scheduler.service.spec.ts b/apps/browser/src/platform/services/task-scheduler/background-task-scheduler.service.spec.ts new file mode 100644 index 00000000000..ded57a5e85d --- /dev/null +++ b/apps/browser/src/platform/services/task-scheduler/background-task-scheduler.service.spec.ts @@ -0,0 +1,129 @@ +import { mock, MockProxy } from "jest-mock-extended"; +import { Observable } from "rxjs"; + +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { ScheduledTaskNames } from "@bitwarden/common/platform/scheduling"; +import { GlobalState, StateProvider } from "@bitwarden/common/platform/state"; + +import { createPortSpyMock } from "../../../autofill/spec/autofill-mocks"; +import { + flushPromises, + sendPortMessage, + triggerPortOnDisconnectEvent, + triggerRuntimeOnConnectEvent, +} from "../../../autofill/spec/testing-utils"; +import { + BrowserTaskSchedulerPortActions, + BrowserTaskSchedulerPortName, +} from "../abstractions/browser-task-scheduler.service"; + +import { BackgroundTaskSchedulerService } from "./background-task-scheduler.service"; + +describe("BackgroundTaskSchedulerService", () => { + let logService: MockProxy; + let stateProvider: MockProxy; + let globalStateMock: MockProxy>; + let portMock: chrome.runtime.Port; + let backgroundTaskSchedulerService: BackgroundTaskSchedulerService; + + beforeEach(() => { + logService = mock(); + globalStateMock = mock>({ + state$: mock>(), + update: jest.fn((callback) => callback([], {} as any)), + }); + stateProvider = mock({ + getGlobal: jest.fn(() => globalStateMock), + }); + portMock = createPortSpyMock(BrowserTaskSchedulerPortName); + backgroundTaskSchedulerService = new BackgroundTaskSchedulerService(logService, stateProvider); + jest.spyOn(globalThis, "setTimeout"); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe("ports on connect", () => { + it("ignores port connections that do not have the correct task scheduler port name", () => { + const portMockWithDifferentName = createPortSpyMock("different-name"); + triggerRuntimeOnConnectEvent(portMockWithDifferentName); + + expect(portMockWithDifferentName.onMessage.addListener).not.toHaveBeenCalled(); + expect(portMockWithDifferentName.onDisconnect.addListener).not.toHaveBeenCalled(); + }); + + it("sets up onMessage and onDisconnect listeners for connected ports", () => { + triggerRuntimeOnConnectEvent(portMock); + + expect(portMock.onMessage.addListener).toHaveBeenCalled(); + expect(portMock.onDisconnect.addListener).toHaveBeenCalled(); + }); + }); + + describe("ports on disconnect", () => { + it("removes the port from the set of connected ports", () => { + triggerRuntimeOnConnectEvent(portMock); + expect(backgroundTaskSchedulerService["ports"].size).toBe(1); + + triggerPortOnDisconnectEvent(portMock); + expect(backgroundTaskSchedulerService["ports"].size).toBe(0); + expect(portMock.onMessage.removeListener).toHaveBeenCalled(); + expect(portMock.onDisconnect.removeListener).toHaveBeenCalled(); + }); + }); + + describe("port message handlers", () => { + beforeEach(() => { + triggerRuntimeOnConnectEvent(portMock); + backgroundTaskSchedulerService.registerTaskHandler( + ScheduledTaskNames.loginStrategySessionTimeout, + jest.fn(), + ); + }); + + it("sets a setTimeout backup alarm", async () => { + sendPortMessage(portMock, { + action: BrowserTaskSchedulerPortActions.setTimeout, + taskName: ScheduledTaskNames.loginStrategySessionTimeout, + delayInMs: 1000, + }); + await flushPromises(); + + expect(globalThis.setTimeout).toHaveBeenCalled(); + expect(chrome.alarms.create).toHaveBeenCalledWith( + ScheduledTaskNames.loginStrategySessionTimeout, + { delayInMinutes: 0.5 }, + expect.any(Function), + ); + }); + + it("sets a setInterval backup alarm", async () => { + sendPortMessage(portMock, { + action: BrowserTaskSchedulerPortActions.setInterval, + taskName: ScheduledTaskNames.loginStrategySessionTimeout, + intervalInMs: 600000, + }); + await flushPromises(); + + expect(chrome.alarms.create).toHaveBeenCalledWith( + ScheduledTaskNames.loginStrategySessionTimeout, + { delayInMinutes: 10, periodInMinutes: 10 }, + expect.any(Function), + ); + }); + + it("clears a scheduled alarm", async () => { + sendPortMessage(portMock, { + action: BrowserTaskSchedulerPortActions.clearAlarm, + alarmName: ScheduledTaskNames.loginStrategySessionTimeout, + }); + await flushPromises(); + + expect(chrome.alarms.clear).toHaveBeenCalledWith( + ScheduledTaskNames.loginStrategySessionTimeout, + expect.any(Function), + ); + }); + }); +}); diff --git a/apps/browser/src/platform/services/task-scheduler/background-task-scheduler.service.ts b/apps/browser/src/platform/services/task-scheduler/background-task-scheduler.service.ts new file mode 100644 index 00000000000..23b580988f8 --- /dev/null +++ b/apps/browser/src/platform/services/task-scheduler/background-task-scheduler.service.ts @@ -0,0 +1,75 @@ +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { StateProvider } from "@bitwarden/common/platform/state"; + +import { BrowserApi } from "../../browser/browser-api"; +import { + BrowserTaskSchedulerPortActions, + BrowserTaskSchedulerPortMessage, + BrowserTaskSchedulerPortName, +} from "../abstractions/browser-task-scheduler.service"; + +import { BrowserTaskSchedulerServiceImplementation } from "./browser-task-scheduler.service"; + +export class BackgroundTaskSchedulerService extends BrowserTaskSchedulerServiceImplementation { + private ports: Set = new Set(); + + constructor(logService: LogService, stateProvider: StateProvider) { + super(logService, stateProvider); + + BrowserApi.addListener(chrome.runtime.onConnect, this.handlePortOnConnect); + } + + /** + * Handles a port connection made from the foreground task scheduler. + * + * @param port - The port that was connected. + */ + private handlePortOnConnect = (port: chrome.runtime.Port) => { + if (port.name !== BrowserTaskSchedulerPortName) { + return; + } + + this.ports.add(port); + port.onMessage.addListener(this.handlePortMessage); + port.onDisconnect.addListener(this.handlePortOnDisconnect); + }; + + /** + * Handles a port disconnection. + * + * @param port - The port that was disconnected. + */ + private handlePortOnDisconnect = (port: chrome.runtime.Port) => { + port.onMessage.removeListener(this.handlePortMessage); + port.onDisconnect.removeListener(this.handlePortOnDisconnect); + this.ports.delete(port); + }; + + /** + * Handles a message from a port. + * + * @param message - The message that was received. + * @param port - The port that sent the message. + */ + private handlePortMessage = ( + message: BrowserTaskSchedulerPortMessage, + port: chrome.runtime.Port, + ) => { + const isTaskSchedulerPort = port.name === BrowserTaskSchedulerPortName; + const { action, taskName, alarmName, delayInMs, intervalInMs } = message; + + if (isTaskSchedulerPort && action === BrowserTaskSchedulerPortActions.setTimeout) { + super.setTimeout(taskName, delayInMs); + return; + } + + if (isTaskSchedulerPort && action === BrowserTaskSchedulerPortActions.setInterval) { + super.setInterval(taskName, intervalInMs); + return; + } + + if (isTaskSchedulerPort && action === BrowserTaskSchedulerPortActions.clearAlarm) { + super.clearScheduledAlarm(alarmName).catch((error) => this.logService.error(error)); + } + }; +} diff --git a/apps/browser/src/platform/services/task-scheduler/browser-task-scheduler.service.spec.ts b/apps/browser/src/platform/services/task-scheduler/browser-task-scheduler.service.spec.ts new file mode 100644 index 00000000000..d72ba942051 --- /dev/null +++ b/apps/browser/src/platform/services/task-scheduler/browser-task-scheduler.service.spec.ts @@ -0,0 +1,463 @@ +import { mock, MockProxy } from "jest-mock-extended"; +import { BehaviorSubject, Observable } from "rxjs"; + +import { ScheduledTaskNames } from "@bitwarden/common/platform/scheduling"; +import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service"; +import { GlobalState, StateProvider } from "@bitwarden/common/platform/state"; + +import { flushPromises, triggerOnAlarmEvent } from "../../../autofill/spec/testing-utils"; +import { + ActiveAlarm, + BrowserTaskSchedulerService, +} from "../abstractions/browser-task-scheduler.service"; + +import { BrowserTaskSchedulerServiceImplementation } from "./browser-task-scheduler.service"; + +jest.mock("rxjs", () => { + const actualModule = jest.requireActual("rxjs"); + return { + ...actualModule, + firstValueFrom: jest.fn((state$: BehaviorSubject) => state$.value), + }; +}); + +function setupGlobalBrowserMock(overrides: Partial = {}) { + globalThis.browser.alarms = { + create: jest.fn(), + clear: jest.fn(), + get: jest.fn(), + getAll: jest.fn(), + clearAll: jest.fn(), + onAlarm: { + addListener: jest.fn(), + removeListener: jest.fn(), + hasListener: jest.fn(), + }, + ...overrides, + }; +} + +describe("BrowserTaskSchedulerService", () => { + const callback = jest.fn(); + const delayInMinutes = 2; + let activeAlarmsMock$: BehaviorSubject; + let logService: MockProxy; + let stateProvider: MockProxy; + let globalStateMock: MockProxy>; + let browserTaskSchedulerService: BrowserTaskSchedulerService; + let activeAlarms: ActiveAlarm[] = []; + const eventUploadsIntervalCreateInfo = { periodInMinutes: 5, delayInMinutes: 5 }; + const scheduleNextSyncIntervalCreateInfo = { periodInMinutes: 5, delayInMinutes: 5 }; + + beforeEach(() => { + jest.useFakeTimers(); + activeAlarms = [ + mock({ + alarmName: ScheduledTaskNames.eventUploadsInterval, + createInfo: eventUploadsIntervalCreateInfo, + }), + mock({ + alarmName: ScheduledTaskNames.scheduleNextSyncInterval, + createInfo: scheduleNextSyncIntervalCreateInfo, + }), + mock({ + alarmName: ScheduledTaskNames.fido2ClientAbortTimeout, + startTime: Date.now() - 60001, + createInfo: { delayInMinutes: 1, periodInMinutes: undefined }, + }), + ]; + activeAlarmsMock$ = new BehaviorSubject(activeAlarms); + logService = mock(); + globalStateMock = mock>({ + state$: mock>(), + update: jest.fn((callback) => callback([], {} as any)), + }); + stateProvider = mock({ + getGlobal: jest.fn(() => globalStateMock), + }); + browserTaskSchedulerService = new BrowserTaskSchedulerServiceImplementation( + logService, + stateProvider, + ); + browserTaskSchedulerService.activeAlarms$ = activeAlarmsMock$; + browserTaskSchedulerService.registerTaskHandler( + ScheduledTaskNames.loginStrategySessionTimeout, + callback, + ); + // @ts-expect-error mocking global browser object + // eslint-disable-next-line no-global-assign + globalThis.browser = {}; + chrome.alarms.get = jest.fn().mockImplementation((_name, callback) => callback(undefined)); + }); + + afterEach(() => { + jest.clearAllMocks(); + jest.clearAllTimers(); + jest.useRealTimers(); + + // eslint-disable-next-line no-global-assign + globalThis.browser = undefined; + }); + + describe("setTimeout", () => { + it("triggers an error when setting a timeout for a task that is not registered", async () => { + expect(() => + browserTaskSchedulerService.setTimeout( + ScheduledTaskNames.notificationsReconnectTimeout, + 1000, + ), + ).toThrow( + `Task handler for ${ScheduledTaskNames.notificationsReconnectTimeout} not registered. Unable to schedule task.`, + ); + }); + + it("creates a timeout alarm", async () => { + browserTaskSchedulerService.setTimeout( + ScheduledTaskNames.loginStrategySessionTimeout, + delayInMinutes * 60 * 1000, + ); + await flushPromises(); + + expect(chrome.alarms.create).toHaveBeenCalledWith( + ScheduledTaskNames.loginStrategySessionTimeout, + { delayInMinutes }, + expect.any(Function), + ); + }); + + it("skips creating a duplicate timeout alarm", async () => { + const mockAlarm = mock(); + chrome.alarms.get = jest.fn().mockImplementation((_name, callback) => callback(mockAlarm)); + + browserTaskSchedulerService.setTimeout( + ScheduledTaskNames.loginStrategySessionTimeout, + delayInMinutes * 60 * 1000, + ); + + expect(chrome.alarms.create).not.toHaveBeenCalled(); + }); + + describe("when the task is scheduled to be triggered in less than the minimum possible delay", () => { + const delayInMs = 25000; + + it("sets a timeout using the global setTimeout API", async () => { + jest.spyOn(globalThis, "setTimeout"); + + browserTaskSchedulerService.setTimeout( + ScheduledTaskNames.loginStrategySessionTimeout, + delayInMs, + ); + await flushPromises(); + + expect(globalThis.setTimeout).toHaveBeenCalledWith(expect.any(Function), delayInMs); + }); + + it("sets a fallback alarm", async () => { + const delayInMs = 15000; + browserTaskSchedulerService.setTimeout( + ScheduledTaskNames.loginStrategySessionTimeout, + delayInMs, + ); + await flushPromises(); + + expect(chrome.alarms.create).toHaveBeenCalledWith( + ScheduledTaskNames.loginStrategySessionTimeout, + { delayInMinutes: 0.5 }, + expect.any(Function), + ); + }); + + it("sets the fallback for a minimum of 1 minute if the environment not for Chrome", async () => { + setupGlobalBrowserMock(); + + browserTaskSchedulerService.setTimeout( + ScheduledTaskNames.loginStrategySessionTimeout, + delayInMs, + ); + await flushPromises(); + + expect(browser.alarms.create).toHaveBeenCalledWith( + ScheduledTaskNames.loginStrategySessionTimeout, + { delayInMinutes: 1 }, + ); + }); + + it("clears the fallback alarm when the setTimeout is triggered", async () => { + jest.useFakeTimers(); + + browserTaskSchedulerService.setTimeout( + ScheduledTaskNames.loginStrategySessionTimeout, + delayInMs, + ); + jest.advanceTimersByTime(delayInMs); + await flushPromises(); + + expect(chrome.alarms.clear).toHaveBeenCalledWith( + ScheduledTaskNames.loginStrategySessionTimeout, + expect.any(Function), + ); + }); + }); + + it("returns a subscription that can be used to clear the timeout", () => { + jest.spyOn(globalThis, "clearTimeout"); + + const timeoutSubscription = browserTaskSchedulerService.setTimeout( + ScheduledTaskNames.loginStrategySessionTimeout, + 10000, + ); + + timeoutSubscription.unsubscribe(); + + expect(chrome.alarms.clear).toHaveBeenCalledWith( + ScheduledTaskNames.loginStrategySessionTimeout, + expect.any(Function), + ); + expect(globalThis.clearTimeout).toHaveBeenCalled(); + }); + + it("clears alarms in non-chrome environments", () => { + setupGlobalBrowserMock(); + + const timeoutSubscription = browserTaskSchedulerService.setTimeout( + ScheduledTaskNames.loginStrategySessionTimeout, + 10000, + ); + timeoutSubscription.unsubscribe(); + + expect(browser.alarms.clear).toHaveBeenCalledWith( + ScheduledTaskNames.loginStrategySessionTimeout, + ); + }); + }); + + describe("setInterval", () => { + it("triggers an error when setting an interval for a task that is not registered", async () => { + expect(() => { + browserTaskSchedulerService.setInterval( + ScheduledTaskNames.notificationsReconnectTimeout, + 1000, + ); + }).toThrow( + `Task handler for ${ScheduledTaskNames.notificationsReconnectTimeout} not registered. Unable to schedule task.`, + ); + }); + + describe("setting an interval that is less than 1 minute", () => { + const intervalInMs = 10000; + + it("sets up stepped alarms that trigger behavior after the first minute of setInterval execution", async () => { + browserTaskSchedulerService.setInterval( + ScheduledTaskNames.loginStrategySessionTimeout, + intervalInMs, + ); + await flushPromises(); + + expect(chrome.alarms.create).toHaveBeenCalledWith( + `${ScheduledTaskNames.loginStrategySessionTimeout}__0`, + { periodInMinutes: 0.6666666666666666, delayInMinutes: 0.5 }, + expect.any(Function), + ); + expect(chrome.alarms.create).toHaveBeenCalledWith( + `${ScheduledTaskNames.loginStrategySessionTimeout}__1`, + { periodInMinutes: 0.6666666666666666, delayInMinutes: 0.6666666666666666 }, + expect.any(Function), + ); + expect(chrome.alarms.create).toHaveBeenCalledWith( + `${ScheduledTaskNames.loginStrategySessionTimeout}__2`, + { periodInMinutes: 0.6666666666666666, delayInMinutes: 0.8333333333333333 }, + expect.any(Function), + ); + expect(chrome.alarms.create).toHaveBeenCalledWith( + `${ScheduledTaskNames.loginStrategySessionTimeout}__3`, + { periodInMinutes: 0.6666666666666666, delayInMinutes: 1 }, + expect.any(Function), + ); + }); + + it("sets an interval using the global setInterval API", async () => { + jest.spyOn(globalThis, "setInterval"); + + browserTaskSchedulerService.setInterval( + ScheduledTaskNames.loginStrategySessionTimeout, + intervalInMs, + ); + await flushPromises(); + + expect(globalThis.setInterval).toHaveBeenCalledWith(expect.any(Function), intervalInMs); + }); + + it("clears the global setInterval instance once the interval has elapsed the minimum required delay for an alarm", async () => { + jest.useFakeTimers(); + jest.spyOn(globalThis, "clearInterval"); + + browserTaskSchedulerService.setInterval( + ScheduledTaskNames.loginStrategySessionTimeout, + intervalInMs, + ); + await flushPromises(); + jest.advanceTimersByTime(50000); + + expect(globalThis.clearInterval).toHaveBeenCalledWith(expect.any(Number)); + }); + }); + + it("creates an interval alarm", async () => { + const periodInMinutes = 2; + const initialDelayInMs = 1000; + + browserTaskSchedulerService.setInterval( + ScheduledTaskNames.loginStrategySessionTimeout, + periodInMinutes * 60 * 1000, + initialDelayInMs, + ); + await flushPromises(); + + expect(chrome.alarms.create).toHaveBeenCalledWith( + ScheduledTaskNames.loginStrategySessionTimeout, + { periodInMinutes, delayInMinutes: 0.5 }, + expect.any(Function), + ); + }); + + it("defaults the alarm's delay in minutes to the interval in minutes if the delay is not specified", async () => { + const periodInMinutes = 2; + browserTaskSchedulerService.setInterval( + ScheduledTaskNames.loginStrategySessionTimeout, + periodInMinutes * 60 * 1000, + ); + await flushPromises(); + + expect(chrome.alarms.create).toHaveBeenCalledWith( + ScheduledTaskNames.loginStrategySessionTimeout, + { periodInMinutes, delayInMinutes: periodInMinutes }, + expect.any(Function), + ); + }); + + it("returns a subscription that can be used to clear an interval alarm", () => { + jest.spyOn(globalThis, "clearInterval"); + + const intervalSubscription = browserTaskSchedulerService.setInterval( + ScheduledTaskNames.loginStrategySessionTimeout, + 600000, + ); + + intervalSubscription.unsubscribe(); + + expect(chrome.alarms.clear).toHaveBeenCalledWith( + ScheduledTaskNames.loginStrategySessionTimeout, + expect.any(Function), + ); + expect(globalThis.clearInterval).not.toHaveBeenCalled(); + }); + + it("returns a subscription that can be used to clear all stepped interval alarms", () => { + jest.spyOn(globalThis, "clearInterval"); + + const intervalSubscription = browserTaskSchedulerService.setInterval( + ScheduledTaskNames.loginStrategySessionTimeout, + 10000, + ); + + intervalSubscription.unsubscribe(); + + expect(chrome.alarms.clear).toHaveBeenCalledWith( + `${ScheduledTaskNames.loginStrategySessionTimeout}__0`, + expect.any(Function), + ); + expect(chrome.alarms.clear).toHaveBeenCalledWith( + `${ScheduledTaskNames.loginStrategySessionTimeout}__1`, + expect.any(Function), + ); + expect(chrome.alarms.clear).toHaveBeenCalledWith( + `${ScheduledTaskNames.loginStrategySessionTimeout}__2`, + expect.any(Function), + ); + expect(chrome.alarms.clear).toHaveBeenCalledWith( + `${ScheduledTaskNames.loginStrategySessionTimeout}__3`, + expect.any(Function), + ); + expect(globalThis.clearInterval).toHaveBeenCalled(); + }); + }); + + describe("verifyAlarmsState", () => { + it("skips recovering a scheduled task if an existing alarm for the task is present", async () => { + chrome.alarms.get = jest + .fn() + .mockImplementation((_name, callback) => callback(mock())); + + await browserTaskSchedulerService.verifyAlarmsState(); + + expect(chrome.alarms.create).not.toHaveBeenCalled(); + expect(callback).not.toHaveBeenCalled(); + }); + + describe("extension alarm is not set", () => { + it("triggers the task when the task should have triggered", async () => { + const fido2Callback = jest.fn(); + browserTaskSchedulerService.registerTaskHandler( + ScheduledTaskNames.fido2ClientAbortTimeout, + fido2Callback, + ); + + await browserTaskSchedulerService.verifyAlarmsState(); + + expect(fido2Callback).toHaveBeenCalled(); + }); + + it("schedules an alarm for the task when it has not yet triggered ", async () => { + const syncCallback = jest.fn(); + browserTaskSchedulerService.registerTaskHandler( + ScheduledTaskNames.scheduleNextSyncInterval, + syncCallback, + ); + + await browserTaskSchedulerService.verifyAlarmsState(); + + expect(chrome.alarms.create).toHaveBeenCalledWith( + ScheduledTaskNames.scheduleNextSyncInterval, + scheduleNextSyncIntervalCreateInfo, + expect.any(Function), + ); + }); + }); + }); + + describe("triggering a task", () => { + it("triggers a task when an onAlarm event is triggered", () => { + const alarm = mock({ + name: ScheduledTaskNames.loginStrategySessionTimeout, + }); + + triggerOnAlarmEvent(alarm); + + expect(callback).toHaveBeenCalled(); + }); + }); + + describe("clearAllScheduledTasks", () => { + it("clears all scheduled tasks and extension alarms", async () => { + // @ts-expect-error mocking global state update method + globalStateMock.update = jest.fn((callback) => { + const stateValue = callback([], {} as any); + activeAlarmsMock$.next(stateValue); + return stateValue; + }); + + await browserTaskSchedulerService.clearAllScheduledTasks(); + + expect(chrome.alarms.clearAll).toHaveBeenCalled(); + expect(activeAlarmsMock$.value).toEqual([]); + }); + + it("clears all extension alarms within a non Chrome environment", async () => { + setupGlobalBrowserMock(); + + await browserTaskSchedulerService.clearAllScheduledTasks(); + + expect(browser.alarms.clearAll).toHaveBeenCalled(); + }); + }); +}); diff --git a/apps/browser/src/platform/services/task-scheduler/browser-task-scheduler.service.ts b/apps/browser/src/platform/services/task-scheduler/browser-task-scheduler.service.ts new file mode 100644 index 00000000000..187742f5891 --- /dev/null +++ b/apps/browser/src/platform/services/task-scheduler/browser-task-scheduler.service.ts @@ -0,0 +1,427 @@ +import { firstValueFrom, map, Observable, Subscription } from "rxjs"; + +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { + DefaultTaskSchedulerService, + ScheduledTaskName, +} from "@bitwarden/common/platform/scheduling"; +import { + TASK_SCHEDULER_DISK, + GlobalState, + KeyDefinition, + StateProvider, +} from "@bitwarden/common/platform/state"; + +import { BrowserApi } from "../../browser/browser-api"; +import { + ActiveAlarm, + BrowserTaskSchedulerService, +} from "../abstractions/browser-task-scheduler.service"; + +const ACTIVE_ALARMS = new KeyDefinition(TASK_SCHEDULER_DISK, "activeAlarms", { + deserializer: (value: ActiveAlarm[]) => value ?? [], +}); + +export class BrowserTaskSchedulerServiceImplementation + extends DefaultTaskSchedulerService + implements BrowserTaskSchedulerService +{ + private activeAlarmsState: GlobalState; + readonly activeAlarms$: Observable; + + constructor( + logService: LogService, + private stateProvider: StateProvider, + ) { + super(logService); + + this.activeAlarmsState = this.stateProvider.getGlobal(ACTIVE_ALARMS); + this.activeAlarms$ = this.activeAlarmsState.state$.pipe( + map((activeAlarms) => activeAlarms ?? []), + ); + + this.setupOnAlarmListener(); + } + + /** + * Sets a timeout to execute a callback after a delay. If the delay is less + * than 1 minute, it will use the global setTimeout. Otherwise, it will + * create a browser extension alarm to handle the delay. + * + * @param taskName - The name of the task, used in defining the alarm. + * @param delayInMs - The delay in milliseconds. + */ + setTimeout(taskName: ScheduledTaskName, delayInMs: number): Subscription { + let timeoutHandle: number | NodeJS.Timeout; + this.validateRegisteredTask(taskName); + + const delayInMinutes = delayInMs / 1000 / 60; + this.scheduleAlarm(taskName, { + delayInMinutes: this.getUpperBoundDelayInMinutes(delayInMinutes), + }).catch((error) => this.logService.error("Failed to schedule alarm", error)); + + // If the delay is less than a minute, we want to attempt to trigger the task through a setTimeout. + // The alarm previously scheduled will be used as a backup in case the setTimeout fails. + if (delayInMinutes < this.getUpperBoundDelayInMinutes(delayInMinutes)) { + timeoutHandle = globalThis.setTimeout(async () => { + await this.clearScheduledAlarm(taskName); + await this.triggerTask(taskName); + }, delayInMs); + } + + return new Subscription(() => { + if (timeoutHandle) { + globalThis.clearTimeout(timeoutHandle); + } + this.clearScheduledAlarm(taskName).catch((error) => + this.logService.error("Failed to clear alarm", error), + ); + }); + } + + /** + * Sets an interval to execute a callback at each interval. If the interval is + * less than 1 minute, it will use the global setInterval. Otherwise, it will + * create a browser extension alarm to handle the interval. + * + * @param taskName - The name of the task, used in defining the alarm. + * @param intervalInMs - The interval in milliseconds. + * @param initialDelayInMs - The initial delay in milliseconds. + */ + setInterval( + taskName: ScheduledTaskName, + intervalInMs: number, + initialDelayInMs?: number, + ): Subscription { + this.validateRegisteredTask(taskName); + + const intervalInMinutes = intervalInMs / 1000 / 60; + const initialDelayInMinutes = initialDelayInMs + ? initialDelayInMs / 1000 / 60 + : intervalInMinutes; + + if (intervalInMinutes < this.getUpperBoundDelayInMinutes(intervalInMinutes)) { + return this.setupSteppedIntervalAlarms(taskName, intervalInMs); + } + + this.scheduleAlarm(taskName, { + periodInMinutes: this.getUpperBoundDelayInMinutes(intervalInMinutes), + delayInMinutes: this.getUpperBoundDelayInMinutes(initialDelayInMinutes), + }).catch((error) => this.logService.error("Failed to schedule alarm", error)); + + return new Subscription(() => + this.clearScheduledAlarm(taskName).catch((error) => + this.logService.error("Failed to clear alarm", error), + ), + ); + } + + /** + * Used in cases where the interval is less than 1 minute. This method will set up a setInterval + * to initialize expected recurring behavior, then create a series of alarms to handle the + * expected scheduled task through the alarms api. This is necessary because the alarms + * api does not support intervals less than 1 minute. + * + * @param taskName - The name of the task + * @param intervalInMs - The interval in milliseconds. + */ + private setupSteppedIntervalAlarms( + taskName: ScheduledTaskName, + intervalInMs: number, + ): Subscription { + const alarmMinDelayInMinutes = this.getAlarmMinDelayInMinutes(); + const intervalInMinutes = intervalInMs / 1000 / 60; + const numberOfAlarmsToCreate = Math.ceil(Math.ceil(1 / intervalInMinutes) / 2) + 1; + const steppedAlarmPeriodInMinutes = alarmMinDelayInMinutes + intervalInMinutes; + const steppedAlarmNames: string[] = []; + for (let alarmIndex = 0; alarmIndex < numberOfAlarmsToCreate; alarmIndex++) { + const steppedAlarmName = `${taskName}__${alarmIndex}`; + steppedAlarmNames.push(steppedAlarmName); + + const delayInMinutes = this.getUpperBoundDelayInMinutes( + alarmMinDelayInMinutes + intervalInMinutes * alarmIndex, + ); + + this.clearScheduledAlarm(steppedAlarmName) + .then(() => + this.scheduleAlarm(steppedAlarmName, { + periodInMinutes: steppedAlarmPeriodInMinutes, + delayInMinutes, + }).catch((error) => this.logService.error("Failed to schedule alarm", error)), + ) + .catch((error) => this.logService.error("Failed to clear alarm", error)); + } + + let elapsedMs = 0; + const intervalHandle: number | NodeJS.Timeout = globalThis.setInterval(async () => { + elapsedMs += intervalInMs; + const elapsedMinutes = elapsedMs / 1000 / 60; + + if (elapsedMinutes >= alarmMinDelayInMinutes) { + globalThis.clearInterval(intervalHandle); + return; + } + + await this.triggerTask(taskName, intervalInMinutes); + }, intervalInMs); + + return new Subscription(() => { + if (intervalHandle) { + globalThis.clearInterval(intervalHandle); + } + steppedAlarmNames.forEach((alarmName) => + this.clearScheduledAlarm(alarmName).catch((error) => + this.logService.error("Failed to clear alarm", error), + ), + ); + }); + } + + /** + * Clears all scheduled tasks by clearing all browser extension + * alarms and resetting the active alarms state. + */ + async clearAllScheduledTasks(): Promise { + await this.clearAllAlarms(); + await this.updateActiveAlarms([]); + } + + /** + * Verifies the state of the active alarms by checking if + * any alarms have been missed or need to be created. + */ + async verifyAlarmsState(): Promise { + const currentTime = Date.now(); + const activeAlarms = await this.getActiveAlarms(); + + for (const alarm of activeAlarms) { + const { alarmName, startTime, createInfo } = alarm; + const existingAlarm = await this.getAlarm(alarmName); + if (existingAlarm) { + continue; + } + + const shouldAlarmHaveBeenTriggered = createInfo.when && createInfo.when < currentTime; + const hasSetTimeoutAlarmExceededDelay = + !createInfo.periodInMinutes && + createInfo.delayInMinutes && + startTime + createInfo.delayInMinutes * 60 * 1000 < currentTime; + if (shouldAlarmHaveBeenTriggered || hasSetTimeoutAlarmExceededDelay) { + await this.triggerTask(alarmName); + continue; + } + + this.scheduleAlarm(alarmName, createInfo).catch((error) => + this.logService.error("Failed to schedule alarm", error), + ); + } + } + + /** + * Creates a browser extension alarm with the given name and create info. + * + * @param alarmName - The name of the alarm. + * @param createInfo - The alarm create info. + */ + private async scheduleAlarm( + alarmName: string, + createInfo: chrome.alarms.AlarmCreateInfo, + ): Promise { + const existingAlarm = await this.getAlarm(alarmName); + if (existingAlarm) { + this.logService.debug(`Alarm ${alarmName} already exists. Skipping creation.`); + return; + } + + await this.createAlarm(alarmName, createInfo); + await this.setActiveAlarm(alarmName, createInfo); + } + + /** + * Gets the active alarms from state. + */ + private async getActiveAlarms(): Promise { + return await firstValueFrom(this.activeAlarms$); + } + + /** + * Sets an active alarm in state. + * + * @param alarmName - The name of the active alarm to set. + * @param createInfo - The creation info of the active alarm. + */ + private async setActiveAlarm( + alarmName: string, + createInfo: chrome.alarms.AlarmCreateInfo, + ): Promise { + const activeAlarms = await this.getActiveAlarms(); + const filteredAlarms = activeAlarms.filter((alarm) => alarm.alarmName !== alarmName); + filteredAlarms.push({ + alarmName, + startTime: Date.now(), + createInfo, + }); + await this.updateActiveAlarms(filteredAlarms); + } + + /** + * Deletes an active alarm from state. + * + * @param alarmName - The name of the active alarm to delete. + */ + private async deleteActiveAlarm(alarmName: string): Promise { + const activeAlarms = await this.getActiveAlarms(); + const filteredAlarms = activeAlarms.filter((alarm) => alarm.alarmName !== alarmName); + await this.updateActiveAlarms(filteredAlarms || []); + } + + /** + * Clears a scheduled alarm by its name and deletes it from the active alarms state. + * + * @param alarmName - The name of the alarm to clear. + */ + async clearScheduledAlarm(alarmName: string): Promise { + const wasCleared = await this.clearAlarm(alarmName); + if (wasCleared) { + await this.deleteActiveAlarm(alarmName); + } + } + + /** + * Updates the active alarms state with the given alarms. + * + * @param alarms - The alarms to update the state with. + */ + private async updateActiveAlarms(alarms: ActiveAlarm[]): Promise { + await this.activeAlarmsState.update(() => alarms); + } + + /** + * Sets up the on alarm listener to handle alarms. + */ + private setupOnAlarmListener(): void { + BrowserApi.addListener(chrome.alarms.onAlarm, this.handleOnAlarm); + } + + /** + * Handles on alarm events, triggering the alarm if a handler exists. + * + * @param alarm - The alarm to handle. + */ + private handleOnAlarm = async (alarm: chrome.alarms.Alarm): Promise => { + const { name, periodInMinutes } = alarm; + await this.triggerTask(name, periodInMinutes); + }; + + /** + * Triggers an alarm by calling its handler and + * deleting it if it is a one-time alarm. + * + * @param alarmName - The name of the alarm to trigger. + * @param periodInMinutes - The period in minutes of an interval alarm. + */ + protected async triggerTask(alarmName: string, periodInMinutes?: number): Promise { + const taskName = this.getTaskFromAlarmName(alarmName); + const handler = this.taskHandlers.get(taskName); + if (!periodInMinutes) { + await this.deleteActiveAlarm(alarmName); + } + + if (handler) { + handler(); + } + } + + /** + * Parses and returns the task name from an alarm name. + * + * @param alarmName - The alarm name to parse. + */ + protected getTaskFromAlarmName(alarmName: string): ScheduledTaskName { + return alarmName.split("__")[0] as ScheduledTaskName; + } + + /** + * Clears a new alarm with the given name and create info. Returns a promise + * that indicates when the alarm has been cleared successfully. + * + * @param alarmName - The name of the alarm to create. + */ + private async clearAlarm(alarmName: string): Promise { + if (this.isNonChromeEnvironment()) { + return browser.alarms.clear(alarmName); + } + + return new Promise((resolve) => chrome.alarms.clear(alarmName, resolve)); + } + + /** + * Clears all alarms that have been set by the extension. Returns a promise + * that indicates when all alarms have been cleared successfully. + */ + private clearAllAlarms(): Promise { + if (this.isNonChromeEnvironment()) { + return browser.alarms.clearAll(); + } + + return new Promise((resolve) => chrome.alarms.clearAll(resolve)); + } + + /** + * Creates a new alarm with the given name and create info. + * + * @param alarmName - The name of the alarm to create. + * @param createInfo - The creation info for the alarm. + */ + private async createAlarm( + alarmName: string, + createInfo: chrome.alarms.AlarmCreateInfo, + ): Promise { + if (this.isNonChromeEnvironment()) { + return browser.alarms.create(alarmName, createInfo); + } + + return new Promise((resolve) => chrome.alarms.create(alarmName, createInfo, resolve)); + } + + /** + * Gets the alarm with the given name. + * + * @param alarmName - The name of the alarm to get. + */ + private getAlarm(alarmName: string): Promise { + if (this.isNonChromeEnvironment()) { + return browser.alarms.get(alarmName); + } + + return new Promise((resolve) => chrome.alarms.get(alarmName, resolve)); + } + + /** + * Checks if the environment is a non-Chrome environment. This is used to determine + * if the browser alarms API should be used in place of the chrome alarms API. This + * is necessary because the `chrome` polyfill that Mozilla implements does not allow + * passing the callback parameter in the same way most `chrome.alarm` api calls allow. + */ + private isNonChromeEnvironment(): boolean { + return typeof browser !== "undefined" && !!browser.alarms; + } + + /** + * Gets the minimum delay in minutes for an alarm. This is used to ensure that the + * delay is at least 1 minute in non-Chrome environments. In Chrome environments, the + * delay can be as low as 0.5 minutes. + */ + private getAlarmMinDelayInMinutes(): number { + return this.isNonChromeEnvironment() ? 1 : 0.5; + } + + /** + * Gets the upper bound delay in minutes for a given delay in minutes. + * + * @param delayInMinutes - The delay in minutes. + */ + private getUpperBoundDelayInMinutes(delayInMinutes: number): number { + return Math.max(this.getAlarmMinDelayInMinutes(), delayInMinutes); + } +} diff --git a/apps/browser/src/platform/services/task-scheduler/foreground-task-scheduler.service.spec.ts b/apps/browser/src/platform/services/task-scheduler/foreground-task-scheduler.service.spec.ts new file mode 100644 index 00000000000..e0ee49c5fa1 --- /dev/null +++ b/apps/browser/src/platform/services/task-scheduler/foreground-task-scheduler.service.spec.ts @@ -0,0 +1,79 @@ +import { mock, MockProxy } from "jest-mock-extended"; +import { Observable } from "rxjs"; + +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { ScheduledTaskNames } from "@bitwarden/common/platform/scheduling"; +import { GlobalState, StateProvider } from "@bitwarden/common/platform/state"; + +import { createPortSpyMock } from "../../../autofill/spec/autofill-mocks"; +import { flushPromises } from "../../../autofill/spec/testing-utils"; +import { + BrowserTaskSchedulerPortActions, + BrowserTaskSchedulerPortName, +} from "../abstractions/browser-task-scheduler.service"; + +import { ForegroundTaskSchedulerService } from "./foreground-task-scheduler.service"; + +describe("ForegroundTaskSchedulerService", () => { + let logService: MockProxy; + let stateProvider: MockProxy; + let globalStateMock: MockProxy>; + let portMock: chrome.runtime.Port; + let foregroundTaskSchedulerService: ForegroundTaskSchedulerService; + + beforeEach(() => { + logService = mock(); + globalStateMock = mock>({ + state$: mock>(), + update: jest.fn((callback) => callback([], {} as any)), + }); + stateProvider = mock({ + getGlobal: jest.fn(() => globalStateMock), + }); + portMock = createPortSpyMock(BrowserTaskSchedulerPortName); + foregroundTaskSchedulerService = new ForegroundTaskSchedulerService(logService, stateProvider); + foregroundTaskSchedulerService["port"] = portMock; + foregroundTaskSchedulerService.registerTaskHandler( + ScheduledTaskNames.loginStrategySessionTimeout, + jest.fn(), + ); + jest.spyOn(globalThis, "setTimeout"); + jest.spyOn(globalThis, "setInterval"); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it("sets a timeout for a task and sends a message to the background to set up a backup timeout alarm", async () => { + foregroundTaskSchedulerService.setTimeout(ScheduledTaskNames.loginStrategySessionTimeout, 1000); + await flushPromises(); + + expect(globalThis.setTimeout).toHaveBeenCalledWith(expect.any(Function), 1000); + expect(chrome.alarms.create).toHaveBeenCalledWith( + "loginStrategySessionTimeout", + { delayInMinutes: 0.5 }, + expect.any(Function), + ); + expect(portMock.postMessage).toHaveBeenCalledWith({ + action: BrowserTaskSchedulerPortActions.setTimeout, + taskName: ScheduledTaskNames.loginStrategySessionTimeout, + delayInMs: 1000, + }); + }); + + it("sets an interval for a task and sends a message to the background to set up a backup interval alarm", async () => { + foregroundTaskSchedulerService.setInterval( + ScheduledTaskNames.loginStrategySessionTimeout, + 1000, + ); + await flushPromises(); + + expect(globalThis.setInterval).toHaveBeenCalledWith(expect.any(Function), 1000); + expect(portMock.postMessage).toHaveBeenCalledWith({ + action: BrowserTaskSchedulerPortActions.setInterval, + taskName: ScheduledTaskNames.loginStrategySessionTimeout, + intervalInMs: 1000, + }); + }); +}); diff --git a/apps/browser/src/platform/services/task-scheduler/foreground-task-scheduler.service.ts b/apps/browser/src/platform/services/task-scheduler/foreground-task-scheduler.service.ts new file mode 100644 index 00000000000..af4d56aa62a --- /dev/null +++ b/apps/browser/src/platform/services/task-scheduler/foreground-task-scheduler.service.ts @@ -0,0 +1,71 @@ +import { Subscription } from "rxjs"; + +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { ScheduledTaskName } from "@bitwarden/common/platform/scheduling"; +import { StateProvider } from "@bitwarden/common/platform/state"; + +import { + BrowserTaskSchedulerPortActions, + BrowserTaskSchedulerPortMessage, + BrowserTaskSchedulerPortName, +} from "../abstractions/browser-task-scheduler.service"; + +import { BrowserTaskSchedulerServiceImplementation } from "./browser-task-scheduler.service"; + +export class ForegroundTaskSchedulerService extends BrowserTaskSchedulerServiceImplementation { + private port: chrome.runtime.Port; + + constructor(logService: LogService, stateProvider: StateProvider) { + super(logService, stateProvider); + + this.port = chrome.runtime.connect({ name: BrowserTaskSchedulerPortName }); + } + + /** + * Sends a port message to the background to set up a fallback timeout. Also sets a timeout locally. + * This is done to ensure that the timeout triggers even if the popup is closed. + * + * @param taskName - The name of the task. + * @param delayInMs - The delay in milliseconds. + */ + setTimeout(taskName: ScheduledTaskName, delayInMs: number): Subscription { + this.sendPortMessage({ + action: BrowserTaskSchedulerPortActions.setTimeout, + taskName, + delayInMs, + }); + + return super.setTimeout(taskName, delayInMs); + } + + /** + * Sends a port message to the background to set up a fallback interval. Also sets an interval locally. + * This is done to ensure that the interval triggers even if the popup is closed. + * + * @param taskName - The name of the task. + * @param intervalInMs - The interval in milliseconds. + * @param initialDelayInMs - The initial delay in milliseconds. + */ + setInterval( + taskName: ScheduledTaskName, + intervalInMs: number, + initialDelayInMs?: number, + ): Subscription { + this.sendPortMessage({ + action: BrowserTaskSchedulerPortActions.setInterval, + taskName, + intervalInMs, + }); + + return super.setInterval(taskName, intervalInMs, initialDelayInMs); + } + + /** + * Sends a message to the background task scheduler. + * + * @param message - The message to send. + */ + private sendPortMessage(message: BrowserTaskSchedulerPortMessage) { + this.port.postMessage(message); + } +} diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index b083c2f4c8b..c102f461a6e 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -61,6 +61,7 @@ import { BiometricStateService } from "@bitwarden/common/platform/biometrics/bio import { Message, MessageListener, MessageSender } from "@bitwarden/common/platform/messaging"; // eslint-disable-next-line no-restricted-imports -- Used for dependency injection import { SubjectMessageSender } from "@bitwarden/common/platform/messaging/internal"; +import { TaskSchedulerService } from "@bitwarden/common/platform/scheduling"; import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service"; import { ContainerService } from "@bitwarden/common/platform/services/container.service"; import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider"; @@ -102,6 +103,7 @@ import BrowserLocalStorageService from "../../platform/services/browser-local-st import { BrowserScriptInjectorService } from "../../platform/services/browser-script-injector.service"; import I18nService from "../../platform/services/i18n.service"; import { ForegroundPlatformUtilsService } from "../../platform/services/platform-utils/foreground-platform-utils.service"; +import { ForegroundTaskSchedulerService } from "../../platform/services/task-scheduler/foreground-task-scheduler.service"; import { BrowserStorageServiceProvider } from "../../platform/storage/browser-storage-service.provider"; import { ForegroundMemoryStorageService } from "../../platform/storage/foreground-memory-storage.service"; import { fromChromeRuntimeMessaging } from "../../platform/utils/from-chrome-runtime-messaging"; @@ -516,6 +518,15 @@ const safeProviders: SafeProvider[] = [ useClass: Fido2UserVerificationService, deps: [PasswordRepromptService, UserVerificationService, DialogService], }), + safeProvider({ + provide: TaskSchedulerService, + useExisting: ForegroundTaskSchedulerService, + }), + safeProvider({ + provide: ForegroundTaskSchedulerService, + useFactory: getBgService("taskSchedulerService"), + deps: [], + }), ]; @NgModule({ diff --git a/apps/browser/src/services/vault-timeout/vault-timeout.service.ts b/apps/browser/src/services/vault-timeout/vault-timeout.service.ts index 9e9a24fb9c3..e0b9db5422b 100644 --- a/apps/browser/src/services/vault-timeout/vault-timeout.service.ts +++ b/apps/browser/src/services/vault-timeout/vault-timeout.service.ts @@ -4,16 +4,13 @@ import { SafariApp } from "../../browser/safariApp"; export default class VaultTimeoutService extends BaseVaultTimeoutService { startCheck() { - // 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.checkVaultTimeout(); if (this.platformUtilsService.isSafari()) { - // 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.checkSafari(); - } else { - setInterval(() => this.checkVaultTimeout(), 10 * 1000); // check every 10 seconds + this.checkVaultTimeout().catch((error) => this.logService.error(error)); + this.checkSafari().catch((error) => this.logService.error(error)); + return; } + + super.startCheck(); } // This is a work-around to safari adding an arbitrary delay to setTimeout and diff --git a/apps/browser/test.setup.ts b/apps/browser/test.setup.ts index 16ebdcbc605..2c358b62c4e 100644 --- a/apps/browser/test.setup.ts +++ b/apps/browser/test.setup.ts @@ -143,6 +143,18 @@ const webNavigation = { }, }; +const alarms = { + clear: jest.fn().mockImplementation((_name, callback) => callback(true)), + clearAll: jest.fn().mockImplementation((callback) => callback(true)), + create: jest.fn().mockImplementation((_name, _createInfo, callback) => callback()), + get: jest.fn().mockImplementation((_name, callback) => callback(null)), + getAll: jest.fn().mockImplementation((callback) => callback([])), + onAlarm: { + addListener: jest.fn(), + removeListener: jest.fn(), + }, +}; + // set chrome global.chrome = { i18n, @@ -158,4 +170,5 @@ global.chrome = { offscreen, permissions, webNavigation, + alarms, } as any; diff --git a/apps/cli/src/service-container.ts b/apps/cli/src/service-container.ts index b352af7de7b..3d53013ef0c 100644 --- a/apps/cli/src/service-container.ts +++ b/apps/cli/src/service-container.ts @@ -65,6 +65,10 @@ import { StateFactory } from "@bitwarden/common/platform/factories/state-factory import { MessageSender } from "@bitwarden/common/platform/messaging"; import { Account } from "@bitwarden/common/platform/models/domain/account"; import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state"; +import { + TaskSchedulerService, + DefaultTaskSchedulerService, +} from "@bitwarden/common/platform/scheduling"; import { AppIdService } from "@bitwarden/common/platform/services/app-id.service"; import { ConfigApiService } from "@bitwarden/common/platform/services/config/config-api.service"; import { DefaultConfigService } from "@bitwarden/common/platform/services/config/default-config.service"; @@ -239,6 +243,7 @@ export class ServiceContainer { providerApiService: ProviderApiServiceAbstraction; userAutoUnlockKeyService: UserAutoUnlockKeyService; kdfConfigService: KdfConfigServiceAbstraction; + taskSchedulerService: TaskSchedulerService; constructor() { let p = null; @@ -543,6 +548,7 @@ export class ServiceContainer { this.stateProvider, ); + this.taskSchedulerService = new DefaultTaskSchedulerService(this.logService); this.loginStrategyService = new LoginStrategyService( this.accountService, this.masterPasswordService, @@ -568,6 +574,7 @@ export class ServiceContainer { this.billingAccountProfileStateService, this.vaultTimeoutSettingsService, this.kdfConfigService, + this.taskSchedulerService, ); this.authService = new AuthService( @@ -642,6 +649,8 @@ export class ServiceContainer { this.authService, this.vaultTimeoutSettingsService, this.stateEventRunnerService, + this.taskSchedulerService, + this.logService, lockedCallback, null, ); @@ -724,6 +733,7 @@ export class ServiceContainer { this.stateProvider, this.logService, this.authService, + this.taskSchedulerService, ); this.eventCollectionService = new EventCollectionService( diff --git a/apps/desktop/src/app/services/services.module.ts b/apps/desktop/src/app/services/services.module.ts index dfea2e6f274..c0b4bf4eb1c 100644 --- a/apps/desktop/src/app/services/services.module.ts +++ b/apps/desktop/src/app/services/services.module.ts @@ -45,6 +45,7 @@ import { BiometricStateService } from "@bitwarden/common/platform/biometrics/bio import { Message, MessageListener, MessageSender } from "@bitwarden/common/platform/messaging"; // eslint-disable-next-line no-restricted-imports -- Used for dependency injection import { SubjectMessageSender } from "@bitwarden/common/platform/messaging/internal"; +import { TaskSchedulerService } from "@bitwarden/common/platform/scheduling"; import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service"; import { SystemService } from "@bitwarden/common/platform/services/system.service"; import { GlobalStateProvider, StateProvider } from "@bitwarden/common/platform/state"; @@ -177,6 +178,7 @@ const safeProviders: SafeProvider[] = [ VaultTimeoutSettingsService, BiometricStateService, AccountServiceAbstraction, + TaskSchedulerService, ], }), safeProvider({ diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 619155c9414..fa53246062a 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -157,6 +157,10 @@ import { SubjectMessageSender } from "@bitwarden/common/platform/messaging/inter import { devFlagEnabled } from "@bitwarden/common/platform/misc/flags"; import { Account } from "@bitwarden/common/platform/models/domain/account"; import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state"; +import { + TaskSchedulerService, + DefaultTaskSchedulerService, +} from "@bitwarden/common/platform/scheduling"; import { AppIdService } from "@bitwarden/common/platform/services/app-id.service"; import { ConfigApiService } from "@bitwarden/common/platform/services/config/config-api.service"; import { DefaultConfigService } from "@bitwarden/common/platform/services/config/default-config.service"; @@ -409,6 +413,7 @@ const safeProviders: SafeProvider[] = [ BillingAccountProfileStateService, VaultTimeoutSettingsServiceAbstraction, KdfConfigServiceAbstraction, + TaskSchedulerService, ], }), safeProvider({ @@ -714,6 +719,8 @@ const safeProviders: SafeProvider[] = [ AuthServiceAbstraction, VaultTimeoutSettingsServiceAbstraction, StateEventRunnerService, + TaskSchedulerService, + LogService, LOCKED_CALLBACK, LOGOUT_CALLBACK, ], @@ -812,6 +819,7 @@ const safeProviders: SafeProvider[] = [ StateServiceAbstraction, AuthServiceAbstraction, MessagingServiceAbstraction, + TaskSchedulerService, ], }), safeProvider({ @@ -827,7 +835,13 @@ const safeProviders: SafeProvider[] = [ safeProvider({ provide: EventUploadServiceAbstraction, useClass: EventUploadService, - deps: [ApiServiceAbstraction, StateProvider, LogService, AuthServiceAbstraction], + deps: [ + ApiServiceAbstraction, + StateProvider, + LogService, + AuthServiceAbstraction, + TaskSchedulerService, + ], }), safeProvider({ provide: EventCollectionServiceAbstraction, @@ -1215,6 +1229,11 @@ const safeProviders: SafeProvider[] = [ new SubjectMessageSender(subject), deps: [INTRAPROCESS_MESSAGING_SUBJECT], }), + safeProvider({ + provide: TaskSchedulerService, + useClass: DefaultTaskSchedulerService, + deps: [LogService], + }), safeProvider({ provide: ProviderApiServiceAbstraction, useClass: ProviderApiService, diff --git a/libs/auth/src/common/services/login-strategies/login-strategy.service.spec.ts b/libs/auth/src/common/services/login-strategies/login-strategy.service.spec.ts index f0a8d81bea8..778ad7c74c2 100644 --- a/libs/auth/src/common/services/login-strategies/login-strategy.service.spec.ts +++ b/libs/auth/src/common/services/login-strategies/login-strategy.service.spec.ts @@ -27,6 +27,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { KdfType } from "@bitwarden/common/platform/enums"; +import { TaskSchedulerService } from "@bitwarden/common/platform/scheduling"; import { FakeAccountService, FakeGlobalState, @@ -72,6 +73,7 @@ describe("LoginStrategyService", () => { let billingAccountProfileStateService: MockProxy; let vaultTimeoutSettingsService: MockProxy; let kdfConfigService: MockProxy; + let taskSchedulerService: MockProxy; let stateProvider: FakeGlobalStateProvider; let loginStrategyCacheExpirationState: FakeGlobalState; @@ -103,6 +105,7 @@ describe("LoginStrategyService", () => { stateProvider = new FakeGlobalStateProvider(); vaultTimeoutSettingsService = mock(); kdfConfigService = mock(); + taskSchedulerService = mock(); sut = new LoginStrategyService( accountService, @@ -129,6 +132,7 @@ describe("LoginStrategyService", () => { billingAccountProfileStateService, vaultTimeoutSettingsService, kdfConfigService, + taskSchedulerService, ); loginStrategyCacheExpirationState = stateProvider.getFake(CACHE_EXPIRATION_KEY); diff --git a/libs/auth/src/common/services/login-strategies/login-strategy.service.ts b/libs/auth/src/common/services/login-strategies/login-strategy.service.ts index 7169fd69e93..67bcdc3658e 100644 --- a/libs/auth/src/common/services/login-strategies/login-strategy.service.ts +++ b/libs/auth/src/common/services/login-strategies/login-strategy.service.ts @@ -5,6 +5,7 @@ import { map, Observable, shareReplay, + Subscription, } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; @@ -37,6 +38,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { KdfType } from "@bitwarden/common/platform/enums/kdf-type.enum"; +import { TaskSchedulerService, ScheduledTaskNames } from "@bitwarden/common/platform/scheduling"; import { GlobalState, GlobalStateProvider } from "@bitwarden/common/platform/state"; import { DeviceTrustServiceAbstraction } from "@bitwarden/common/src/auth/abstractions/device-trust.service.abstraction"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; @@ -69,7 +71,7 @@ import { const sessionTimeoutLength = 2 * 60 * 1000; // 2 minutes export class LoginStrategyService implements LoginStrategyServiceAbstraction { - private sessionTimeout: unknown; + private sessionTimeoutSubscription: Subscription; private currentAuthnTypeState: GlobalState; private loginStrategyCacheState: GlobalState; private loginStrategyCacheExpirationState: GlobalState; @@ -111,6 +113,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { protected billingAccountProfileStateService: BillingAccountProfileStateService, protected vaultTimeoutSettingsService: VaultTimeoutSettingsService, protected kdfConfigService: KdfConfigService, + protected taskSchedulerService: TaskSchedulerService, ) { this.currentAuthnTypeState = this.stateProvider.get(CURRENT_LOGIN_STRATEGY_KEY); this.loginStrategyCacheState = this.stateProvider.get(CACHE_KEY); @@ -118,6 +121,10 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { this.authRequestPushNotificationState = this.stateProvider.get( AUTH_REQUEST_PUSH_NOTIFICATION_KEY, ); + this.taskSchedulerService.registerTaskHandler( + ScheduledTaskNames.loginStrategySessionTimeout, + () => this.clearCache(), + ); this.currentAuthType$ = this.currentAuthnTypeState.state$; this.loginStrategy$ = this.currentAuthnTypeState.state$.pipe( @@ -268,15 +275,23 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { private async startSessionTimeout(): Promise { await this.clearSessionTimeout(); + + // This Login Strategy Cache Expiration State value set here is used to clear the cache on re-init + // of the application in the case where the timeout is terminated due to a closure of the application + // window. The browser extension popup in particular is susceptible to this concern, as the user + // is almost always likely to close the popup window before the session timeout is reached. await this.loginStrategyCacheExpirationState.update( (_) => new Date(Date.now() + sessionTimeoutLength), ); - this.sessionTimeout = setTimeout(() => this.clearCache(), sessionTimeoutLength); + this.sessionTimeoutSubscription = this.taskSchedulerService.setTimeout( + ScheduledTaskNames.loginStrategySessionTimeout, + sessionTimeoutLength, + ); } private async clearSessionTimeout(): Promise { await this.loginStrategyCacheExpirationState.update((_) => null); - this.sessionTimeout = null; + this.sessionTimeoutSubscription?.unsubscribe(); } private async isSessionValid(): Promise { @@ -284,6 +299,9 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { if (cache == null) { return false; } + + // If the Login Strategy Cache Expiration State value is less than the current + // datetime stamp, then the cache is invalid and should be cleared. const expiration = await firstValueFrom(this.loginStrategyCacheExpirationState.state$); if (expiration != null && expiration < new Date()) { await this.clearCache(); diff --git a/libs/common/src/platform/scheduling/default-task-scheduler.service.spec.ts b/libs/common/src/platform/scheduling/default-task-scheduler.service.spec.ts new file mode 100644 index 00000000000..ec66947f0eb --- /dev/null +++ b/libs/common/src/platform/scheduling/default-task-scheduler.service.spec.ts @@ -0,0 +1,123 @@ +import { mock, MockProxy } from "jest-mock-extended"; + +import { LogService } from "../abstractions/log.service"; +import { ScheduledTaskNames } from "../scheduling/scheduled-task-name.enum"; + +import { DefaultTaskSchedulerService } from "./default-task-scheduler.service"; + +describe("DefaultTaskSchedulerService", () => { + const callback = jest.fn(); + const delayInMs = 1000; + const intervalInMs = 1100; + let logService: MockProxy; + let taskSchedulerService: DefaultTaskSchedulerService; + + beforeEach(() => { + jest.useFakeTimers(); + logService = mock(); + taskSchedulerService = new DefaultTaskSchedulerService(logService); + taskSchedulerService.registerTaskHandler( + ScheduledTaskNames.loginStrategySessionTimeout, + callback, + ); + }); + + afterEach(() => { + jest.clearAllTimers(); + jest.clearAllMocks(); + }); + + it("triggers an error when setting a timeout for a task that is not registered", async () => { + expect(() => + taskSchedulerService.setTimeout(ScheduledTaskNames.notificationsReconnectTimeout, 1000), + ).toThrow( + `Task handler for ${ScheduledTaskNames.notificationsReconnectTimeout} not registered. Unable to schedule task.`, + ); + }); + + it("triggers an error when setting an interval for a task that is not registered", async () => { + expect(() => + taskSchedulerService.setInterval(ScheduledTaskNames.notificationsReconnectTimeout, 1000), + ).toThrow( + `Task handler for ${ScheduledTaskNames.notificationsReconnectTimeout} not registered. Unable to schedule task.`, + ); + }); + + it("overrides the handler for a previously registered task and provides a warning about the task registration", () => { + taskSchedulerService.registerTaskHandler( + ScheduledTaskNames.loginStrategySessionTimeout, + callback, + ); + + expect(logService.warning).toHaveBeenCalledWith( + `Task handler for ${ScheduledTaskNames.loginStrategySessionTimeout} already exists. Overwriting.`, + ); + expect( + taskSchedulerService["taskHandlers"].get(ScheduledTaskNames.loginStrategySessionTimeout), + ).toBeDefined(); + }); + + it("sets a timeout and returns the timeout id", () => { + const timeoutId = taskSchedulerService.setTimeout( + ScheduledTaskNames.loginStrategySessionTimeout, + delayInMs, + ); + + expect(timeoutId).toBeDefined(); + expect(callback).not.toHaveBeenCalled(); + + jest.advanceTimersByTime(delayInMs); + + expect(callback).toHaveBeenCalled(); + }); + + it("sets an interval timeout and results the interval id", () => { + const intervalId = taskSchedulerService.setInterval( + ScheduledTaskNames.loginStrategySessionTimeout, + intervalInMs, + ); + + expect(intervalId).toBeDefined(); + expect(callback).not.toHaveBeenCalled(); + + jest.advanceTimersByTime(intervalInMs); + + expect(callback).toHaveBeenCalled(); + + jest.advanceTimersByTime(intervalInMs); + + expect(callback).toHaveBeenCalledTimes(2); + }); + + it("clears scheduled tasks using the timeout id", () => { + const timeoutHandle = taskSchedulerService.setTimeout( + ScheduledTaskNames.loginStrategySessionTimeout, + delayInMs, + ); + + expect(timeoutHandle).toBeDefined(); + expect(callback).not.toHaveBeenCalled(); + + timeoutHandle.unsubscribe(); + + jest.advanceTimersByTime(delayInMs); + + expect(callback).not.toHaveBeenCalled(); + }); + + it("clears scheduled tasks using the interval id", () => { + const intervalHandle = taskSchedulerService.setInterval( + ScheduledTaskNames.loginStrategySessionTimeout, + intervalInMs, + ); + + expect(intervalHandle).toBeDefined(); + expect(callback).not.toHaveBeenCalled(); + + intervalHandle.unsubscribe(); + + jest.advanceTimersByTime(intervalInMs); + + expect(callback).not.toHaveBeenCalled(); + }); +}); diff --git a/libs/common/src/platform/scheduling/default-task-scheduler.service.ts b/libs/common/src/platform/scheduling/default-task-scheduler.service.ts new file mode 100644 index 00000000000..4de2faec644 --- /dev/null +++ b/libs/common/src/platform/scheduling/default-task-scheduler.service.ts @@ -0,0 +1,97 @@ +import { Subscription } from "rxjs"; + +import { LogService } from "../abstractions/log.service"; +import { ScheduledTaskName } from "../scheduling/scheduled-task-name.enum"; +import { TaskSchedulerService } from "../scheduling/task-scheduler.service"; + +export class DefaultTaskSchedulerService extends TaskSchedulerService { + constructor(protected logService: LogService) { + super(); + + this.taskHandlers = new Map(); + } + + /** + * Sets a timeout and returns the timeout id. + * + * @param taskName - The name of the task. Unused in the base implementation. + * @param delayInMs - The delay in milliseconds. + */ + setTimeout(taskName: ScheduledTaskName, delayInMs: number): Subscription { + this.validateRegisteredTask(taskName); + + const timeoutHandle = globalThis.setTimeout(() => this.triggerTask(taskName), delayInMs); + return new Subscription(() => globalThis.clearTimeout(timeoutHandle)); + } + + /** + * Sets an interval and returns the interval id. + * + * @param taskName - The name of the task. Unused in the base implementation. + * @param intervalInMs - The interval in milliseconds. + * @param _initialDelayInMs - The initial delay in milliseconds. Unused in the base implementation. + */ + setInterval( + taskName: ScheduledTaskName, + intervalInMs: number, + _initialDelayInMs?: number, + ): Subscription { + this.validateRegisteredTask(taskName); + + const intervalHandle = globalThis.setInterval(() => this.triggerTask(taskName), intervalInMs); + + return new Subscription(() => globalThis.clearInterval(intervalHandle)); + } + + /** + * Registers a task handler. + * + * @param taskName - The name of the task. + * @param handler - The task handler. + */ + registerTaskHandler(taskName: ScheduledTaskName, handler: () => void) { + const existingHandler = this.taskHandlers.get(taskName); + if (existingHandler) { + this.logService.warning(`Task handler for ${taskName} already exists. Overwriting.`); + this.unregisterTaskHandler(taskName); + } + + this.taskHandlers.set(taskName, handler); + } + + /** + * Unregisters a task handler. + * + * @param taskName - The name of the task. + */ + unregisterTaskHandler(taskName: ScheduledTaskName) { + this.taskHandlers.delete(taskName); + } + + /** + * Triggers a task. + * + * @param taskName - The name of the task. + * @param _periodInMinutes - The period in minutes. Unused in the base implementation. + */ + protected async triggerTask( + taskName: ScheduledTaskName, + _periodInMinutes?: number, + ): Promise { + const handler = this.taskHandlers.get(taskName); + if (handler) { + handler(); + } + } + + /** + * Validates that a task handler is registered. + * + * @param taskName - The name of the task. + */ + protected validateRegisteredTask(taskName: ScheduledTaskName): void { + if (!this.taskHandlers.has(taskName)) { + throw new Error(`Task handler for ${taskName} not registered. Unable to schedule task.`); + } + } +} diff --git a/libs/common/src/platform/scheduling/index.ts b/libs/common/src/platform/scheduling/index.ts new file mode 100644 index 00000000000..e5f10ca3baf --- /dev/null +++ b/libs/common/src/platform/scheduling/index.ts @@ -0,0 +1,3 @@ +export { TaskSchedulerService } from "./task-scheduler.service"; +export { DefaultTaskSchedulerService } from "./default-task-scheduler.service"; +export { ScheduledTaskNames, ScheduledTaskName } from "./scheduled-task-name.enum"; diff --git a/libs/common/src/platform/scheduling/scheduled-task-name.enum.ts b/libs/common/src/platform/scheduling/scheduled-task-name.enum.ts new file mode 100644 index 00000000000..2c0ffc87eb7 --- /dev/null +++ b/libs/common/src/platform/scheduling/scheduled-task-name.enum.ts @@ -0,0 +1,12 @@ +export const ScheduledTaskNames = { + generatePasswordClearClipboardTimeout: "generatePasswordClearClipboardTimeout", + systemClearClipboardTimeout: "systemClearClipboardTimeout", + loginStrategySessionTimeout: "loginStrategySessionTimeout", + notificationsReconnectTimeout: "notificationsReconnectTimeout", + fido2ClientAbortTimeout: "fido2ClientAbortTimeout", + scheduleNextSyncInterval: "scheduleNextSyncInterval", + eventUploadsInterval: "eventUploadsInterval", + vaultTimeoutCheckInterval: "vaultTimeoutCheckInterval", +} as const; + +export type ScheduledTaskName = (typeof ScheduledTaskNames)[keyof typeof ScheduledTaskNames]; diff --git a/libs/common/src/platform/scheduling/task-scheduler.service.ts b/libs/common/src/platform/scheduling/task-scheduler.service.ts new file mode 100644 index 00000000000..57e5291f7c6 --- /dev/null +++ b/libs/common/src/platform/scheduling/task-scheduler.service.ts @@ -0,0 +1,16 @@ +import { Subscription } from "rxjs"; + +import { ScheduledTaskName } from "./scheduled-task-name.enum"; + +export abstract class TaskSchedulerService { + protected taskHandlers: Map void>; + abstract setTimeout(taskName: ScheduledTaskName, delayInMs: number): Subscription; + abstract setInterval( + taskName: ScheduledTaskName, + intervalInMs: number, + initialDelayInMs?: number, + ): Subscription; + abstract registerTaskHandler(taskName: ScheduledTaskName, handler: () => void): void; + abstract unregisterTaskHandler(taskName: ScheduledTaskName): void; + protected abstract triggerTask(taskName: ScheduledTaskName, periodInMinutes?: number): void; +} diff --git a/libs/common/src/platform/services/fido2/fido2-client.service.spec.ts b/libs/common/src/platform/services/fido2/fido2-client.service.spec.ts index 597f2d8f32e..aac447e0337 100644 --- a/libs/common/src/platform/services/fido2/fido2-client.service.spec.ts +++ b/libs/common/src/platform/services/fido2/fido2-client.service.spec.ts @@ -4,6 +4,7 @@ import { of } from "rxjs"; import { AuthService } from "../../../auth/abstractions/auth.service"; import { AuthenticationStatus } from "../../../auth/enums/authentication-status"; import { DomainSettingsService } from "../../../autofill/services/domain-settings.service"; +import { Utils } from "../../../platform/misc/utils"; import { VaultSettingsService } from "../../../vault/abstractions/vault-settings/vault-settings.service"; import { ConfigService } from "../../abstractions/config/config.service"; import { @@ -17,7 +18,7 @@ import { CreateCredentialParams, FallbackRequestedError, } from "../../abstractions/fido2/fido2-client.service.abstraction"; -import { Utils } from "../../misc/utils"; +import { TaskSchedulerService } from "../../scheduling/task-scheduler.service"; import * as DomainUtils from "./domain-utils"; import { Fido2AuthenticatorService } from "./fido2-authenticator.service"; @@ -35,6 +36,7 @@ describe("FidoAuthenticatorService", () => { let authService!: MockProxy; let vaultSettingsService: MockProxy; let domainSettingsService: MockProxy; + let taskSchedulerService: MockProxy; let client!: Fido2ClientService; let tab!: chrome.tabs.Tab; let isValidRpId!: jest.SpyInstance; @@ -45,6 +47,7 @@ describe("FidoAuthenticatorService", () => { authService = mock(); vaultSettingsService = mock(); domainSettingsService = mock(); + taskSchedulerService = mock(); isValidRpId = jest.spyOn(DomainUtils, "isValidRpId"); @@ -54,6 +57,7 @@ describe("FidoAuthenticatorService", () => { authService, vaultSettingsService, domainSettingsService, + taskSchedulerService, ); configService.serverConfig$ = of({ environment: { vault: VaultUrl } } as any); vaultSettingsService.enablePasskeys$ = of(true); diff --git a/libs/common/src/platform/services/fido2/fido2-client.service.ts b/libs/common/src/platform/services/fido2/fido2-client.service.ts index d22b91fda05..b384fce1f12 100644 --- a/libs/common/src/platform/services/fido2/fido2-client.service.ts +++ b/libs/common/src/platform/services/fido2/fido2-client.service.ts @@ -1,4 +1,4 @@ -import { firstValueFrom } from "rxjs"; +import { firstValueFrom, Subscription } from "rxjs"; import { parse } from "tldts"; import { AuthService } from "../../../auth/abstractions/auth.service"; @@ -27,6 +27,8 @@ import { } from "../../abstractions/fido2/fido2-client.service.abstraction"; import { LogService } from "../../abstractions/log.service"; import { Utils } from "../../misc/utils"; +import { ScheduledTaskNames } from "../../scheduling/scheduled-task-name.enum"; +import { TaskSchedulerService } from "../../scheduling/task-scheduler.service"; import { isValidRpId } from "./domain-utils"; import { Fido2Utils } from "./fido2-utils"; @@ -38,14 +40,33 @@ import { Fido2Utils } from "./fido2-utils"; * It is highly recommended that the W3C specification is used a reference when reading this code. */ export class Fido2ClientService implements Fido2ClientServiceAbstraction { + private timeoutAbortController: AbortController; + private readonly TIMEOUTS = { + NO_VERIFICATION: { + DEFAULT: 120000, + MIN: 30000, + MAX: 180000, + }, + WITH_VERIFICATION: { + DEFAULT: 300000, + MIN: 30000, + MAX: 600000, + }, + }; + constructor( private authenticator: Fido2AuthenticatorService, private configService: ConfigService, private authService: AuthService, private vaultSettingsService: VaultSettingsService, private domainSettingsService: DomainSettingsService, + private taskSchedulerService: TaskSchedulerService, private logService?: LogService, - ) {} + ) { + this.taskSchedulerService.registerTaskHandler(ScheduledTaskNames.fido2ClientAbortTimeout, () => + this.timeoutAbortController?.abort(), + ); + } async isFido2FeatureEnabled(hostname: string, origin: string): Promise { const isUserLoggedIn = @@ -161,7 +182,7 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction { this.logService?.info(`[Fido2Client] Aborted with AbortController`); throw new DOMException("The operation either timed out or was not allowed.", "AbortError"); } - const timeout = setAbortTimeout( + const timeoutSubscription = this.setAbortTimeout( abortController, params.authenticatorSelection?.userVerification, params.timeout, @@ -210,7 +231,8 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction { }; } - clearTimeout(timeout); + timeoutSubscription?.unsubscribe(); + return { credentialId: Fido2Utils.bufferToString(makeCredentialResult.credentialId), attestationObject: Fido2Utils.bufferToString(makeCredentialResult.attestationObject), @@ -273,7 +295,11 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction { throw new DOMException("The operation either timed out or was not allowed.", "AbortError"); } - const timeout = setAbortTimeout(abortController, params.userVerification, params.timeout); + const timeoutSubscription = this.setAbortTimeout( + abortController, + params.userVerification, + params.timeout, + ); let getAssertionResult; try { @@ -310,7 +336,8 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction { this.logService?.info(`[Fido2Client] Aborted with AbortController`); throw new DOMException("The operation either timed out or was not allowed.", "AbortError"); } - clearTimeout(timeout); + + timeoutSubscription?.unsubscribe(); return { authenticatorData: Fido2Utils.bufferToString(getAssertionResult.authenticatorData), @@ -323,43 +350,29 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction { signature: Fido2Utils.bufferToString(getAssertionResult.signature), }; } -} -const TIMEOUTS = { - NO_VERIFICATION: { - DEFAULT: 120000, - MIN: 30000, - MAX: 180000, - }, - WITH_VERIFICATION: { - DEFAULT: 300000, - MIN: 30000, - MAX: 600000, - }, -}; + private setAbortTimeout = ( + abortController: AbortController, + userVerification?: UserVerification, + timeout?: number, + ): Subscription => { + let clampedTimeout: number; -function setAbortTimeout( - abortController: AbortController, - userVerification?: UserVerification, - timeout?: number, -): number { - let clampedTimeout: number; + const { WITH_VERIFICATION, NO_VERIFICATION } = this.TIMEOUTS; + if (userVerification === "required") { + timeout = timeout ?? WITH_VERIFICATION.DEFAULT; + clampedTimeout = Math.max(WITH_VERIFICATION.MIN, Math.min(timeout, WITH_VERIFICATION.MAX)); + } else { + timeout = timeout ?? NO_VERIFICATION.DEFAULT; + clampedTimeout = Math.max(NO_VERIFICATION.MIN, Math.min(timeout, NO_VERIFICATION.MAX)); + } - if (userVerification === "required") { - timeout = timeout ?? TIMEOUTS.WITH_VERIFICATION.DEFAULT; - clampedTimeout = Math.max( - TIMEOUTS.WITH_VERIFICATION.MIN, - Math.min(timeout, TIMEOUTS.WITH_VERIFICATION.MAX), + this.timeoutAbortController = abortController; + return this.taskSchedulerService.setTimeout( + ScheduledTaskNames.fido2ClientAbortTimeout, + clampedTimeout, ); - } else { - timeout = timeout ?? TIMEOUTS.NO_VERIFICATION.DEFAULT; - clampedTimeout = Math.max( - TIMEOUTS.NO_VERIFICATION.MIN, - Math.min(timeout, TIMEOUTS.NO_VERIFICATION.MAX), - ); - } - - return self.setTimeout(() => abortController.abort(), clampedTimeout); + }; } /** diff --git a/libs/common/src/platform/services/system.service.ts b/libs/common/src/platform/services/system.service.ts index a3927a3fb8f..382b3bf8e86 100644 --- a/libs/common/src/platform/services/system.service.ts +++ b/libs/common/src/platform/services/system.service.ts @@ -1,4 +1,4 @@ -import { firstValueFrom, map, timeout } from "rxjs"; +import { firstValueFrom, map, Subscription, timeout } from "rxjs"; import { PinServiceAbstraction } from "../../../../auth/src/common/abstractions"; import { VaultTimeoutSettingsService } from "../../abstractions/vault-timeout/vault-timeout-settings.service"; @@ -13,10 +13,12 @@ import { PlatformUtilsService } from "../abstractions/platform-utils.service"; import { SystemService as SystemServiceAbstraction } from "../abstractions/system.service"; import { BiometricStateService } from "../biometrics/biometric-state.service"; import { Utils } from "../misc/utils"; +import { ScheduledTaskNames } from "../scheduling/scheduled-task-name.enum"; +import { TaskSchedulerService } from "../scheduling/task-scheduler.service"; export class SystemService implements SystemServiceAbstraction { private reloadInterval: any = null; - private clearClipboardTimeout: any = null; + private clearClipboardTimeoutSubscription: Subscription; private clearClipboardTimeoutFunction: () => Promise = null; constructor( @@ -28,7 +30,13 @@ export class SystemService implements SystemServiceAbstraction { private vaultTimeoutSettingsService: VaultTimeoutSettingsService, private biometricStateService: BiometricStateService, private accountService: AccountService, - ) {} + private taskSchedulerService: TaskSchedulerService, + ) { + this.taskSchedulerService.registerTaskHandler( + ScheduledTaskNames.systemClearClipboardTimeout, + () => this.clearPendingClipboard(), + ); + } async startProcessReload(authService: AuthService): Promise { const accounts = await firstValueFrom(this.accountService.accounts$); @@ -111,25 +119,22 @@ export class SystemService implements SystemServiceAbstraction { } async clearClipboard(clipboardValue: string, timeoutMs: number = null): Promise { - if (this.clearClipboardTimeout != null) { - clearTimeout(this.clearClipboardTimeout); - this.clearClipboardTimeout = null; - } + this.clearClipboardTimeoutSubscription?.unsubscribe(); if (Utils.isNullOrWhitespace(clipboardValue)) { return; } - const clearClipboardDelay = await firstValueFrom( - this.autofillSettingsService.clearClipboardDelay$, - ); - - if (clearClipboardDelay == null) { - return; + let taskTimeoutInMs = timeoutMs; + if (!taskTimeoutInMs) { + const clearClipboardDelayInSeconds = await firstValueFrom( + this.autofillSettingsService.clearClipboardDelay$, + ); + taskTimeoutInMs = clearClipboardDelayInSeconds ? clearClipboardDelayInSeconds * 1000 : null; } - if (timeoutMs == null) { - timeoutMs = clearClipboardDelay * 1000; + if (!taskTimeoutInMs) { + return; } this.clearClipboardTimeoutFunction = async () => { @@ -139,9 +144,10 @@ export class SystemService implements SystemServiceAbstraction { } }; - this.clearClipboardTimeout = setTimeout(async () => { - await this.clearPendingClipboard(); - }, timeoutMs); + this.clearClipboardTimeoutSubscription = this.taskSchedulerService.setTimeout( + ScheduledTaskNames.systemClearClipboardTimeout, + taskTimeoutInMs, + ); } async clearPendingClipboard() { diff --git a/libs/common/src/platform/state/state-definitions.ts b/libs/common/src/platform/state/state-definitions.ts index 53e9ca9fb6e..0b55e7be77c 100644 --- a/libs/common/src/platform/state/state-definitions.ts +++ b/libs/common/src/platform/state/state-definitions.ts @@ -112,6 +112,7 @@ export const ENVIRONMENT_DISK = new StateDefinition("environment", "disk"); export const ENVIRONMENT_MEMORY = new StateDefinition("environment", "memory"); export const THEMING_DISK = new StateDefinition("theming", "disk", { web: "disk-local" }); export const TRANSLATION_DISK = new StateDefinition("translation", "disk", { web: "disk-local" }); +export const TASK_SCHEDULER_DISK = new StateDefinition("taskScheduler", "disk"); // Secrets Manager diff --git a/libs/common/src/services/event/event-upload.service.ts b/libs/common/src/services/event/event-upload.service.ts index c87d3b2024d..faac95c4d6e 100644 --- a/libs/common/src/services/event/event-upload.service.ts +++ b/libs/common/src/services/event/event-upload.service.ts @@ -7,6 +7,8 @@ import { AuthenticationStatus } from "../../auth/enums/authentication-status"; import { EventData } from "../../models/data/event.data"; import { EventRequest } from "../../models/request/event.request"; import { LogService } from "../../platform/abstractions/log.service"; +import { ScheduledTaskNames } from "../../platform/scheduling/scheduled-task-name.enum"; +import { TaskSchedulerService } from "../../platform/scheduling/task-scheduler.service"; import { StateProvider } from "../../platform/state"; import { UserId } from "../../types/guid"; @@ -19,7 +21,12 @@ export class EventUploadService implements EventUploadServiceAbstraction { private stateProvider: StateProvider, private logService: LogService, private authService: AuthService, - ) {} + private taskSchedulerService: TaskSchedulerService, + ) { + this.taskSchedulerService.registerTaskHandler(ScheduledTaskNames.eventUploadsInterval, () => + this.uploadEvents(), + ); + } init(checkOnInterval: boolean) { if (this.inited) { @@ -28,10 +35,11 @@ export class EventUploadService implements EventUploadServiceAbstraction { this.inited = true; if (checkOnInterval) { - // 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.uploadEvents(); - setInterval(() => this.uploadEvents(), 60 * 1000); // check every 60 seconds + void this.uploadEvents(); + this.taskSchedulerService.setInterval( + ScheduledTaskNames.eventUploadsInterval, + 60 * 1000, // check every 60 seconds + ); } } diff --git a/libs/common/src/services/notifications.service.ts b/libs/common/src/services/notifications.service.ts index d5c7170e23c..8e6a664a0af 100644 --- a/libs/common/src/services/notifications.service.ts +++ b/libs/common/src/services/notifications.service.ts @@ -1,6 +1,6 @@ import * as signalR from "@microsoft/signalr"; import * as signalRMsgPack from "@microsoft/signalr-protocol-msgpack"; -import { firstValueFrom } from "rxjs"; +import { firstValueFrom, Subscription } from "rxjs"; import { LogoutReason } from "@bitwarden/auth/common"; @@ -20,6 +20,8 @@ import { EnvironmentService } from "../platform/abstractions/environment.service import { LogService } from "../platform/abstractions/log.service"; import { MessagingService } from "../platform/abstractions/messaging.service"; import { StateService } from "../platform/abstractions/state.service"; +import { ScheduledTaskNames } from "../platform/scheduling/scheduled-task-name.enum"; +import { TaskSchedulerService } from "../platform/scheduling/task-scheduler.service"; import { SyncService } from "../vault/abstractions/sync/sync.service.abstraction"; export class NotificationsService implements NotificationsServiceAbstraction { @@ -28,7 +30,8 @@ export class NotificationsService implements NotificationsServiceAbstraction { private connected = false; private inited = false; private inactive = false; - private reconnectTimer: any = null; + private reconnectTimerSubscription: Subscription; + private isSyncingOnReconnect = true; constructor( private logService: LogService, @@ -40,7 +43,12 @@ export class NotificationsService implements NotificationsServiceAbstraction { private stateService: StateService, private authService: AuthService, private messagingService: MessagingService, + private taskSchedulerService: TaskSchedulerService, ) { + this.taskSchedulerService.registerTaskHandler( + ScheduledTaskNames.notificationsReconnectTimeout, + () => this.reconnect(this.isSyncingOnReconnect), + ); this.environmentService.environment$.subscribe(() => { if (!this.inited) { return; @@ -213,10 +221,8 @@ export class NotificationsService implements NotificationsServiceAbstraction { } private async reconnect(sync: boolean) { - if (this.reconnectTimer != null) { - clearTimeout(this.reconnectTimer); - this.reconnectTimer = null; - } + this.reconnectTimerSubscription?.unsubscribe(); + if (this.connected || !this.inited || this.inactive) { return; } @@ -236,7 +242,11 @@ export class NotificationsService implements NotificationsServiceAbstraction { } if (!this.connected) { - this.reconnectTimer = setTimeout(() => this.reconnect(sync), this.random(120000, 300000)); + this.isSyncingOnReconnect = sync; + this.reconnectTimerSubscription = this.taskSchedulerService.setTimeout( + ScheduledTaskNames.notificationsReconnectTimeout, + this.random(120000, 300000), + ); } } diff --git a/libs/common/src/services/vault-timeout/vault-timeout.service.spec.ts b/libs/common/src/services/vault-timeout/vault-timeout.service.spec.ts index 6a8071af0c0..487a2578b5e 100644 --- a/libs/common/src/services/vault-timeout/vault-timeout.service.spec.ts +++ b/libs/common/src/services/vault-timeout/vault-timeout.service.spec.ts @@ -2,6 +2,8 @@ import { MockProxy, any, mock } from "jest-mock-extended"; import { BehaviorSubject, from, of } from "rxjs"; import { LogoutReason } from "@bitwarden/auth/common"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { TaskSchedulerService } from "@bitwarden/common/platform/scheduling"; import { FakeAccountService, mockAccountServiceWith } from "../../../spec/fake-account-service"; import { SearchService } from "../../abstractions/search.service"; @@ -37,6 +39,8 @@ describe("VaultTimeoutService", () => { let authService: MockProxy; let vaultTimeoutSettingsService: MockProxy; let stateEventRunnerService: MockProxy; + let taskSchedulerService: MockProxy; + let logService: MockProxy; let lockedCallback: jest.Mock, [userId: string]>; let loggedOutCallback: jest.Mock, [logoutReason: LogoutReason, userId?: string]>; @@ -60,6 +64,8 @@ describe("VaultTimeoutService", () => { authService = mock(); vaultTimeoutSettingsService = mock(); stateEventRunnerService = mock(); + taskSchedulerService = mock(); + logService = mock(); lockedCallback = jest.fn(); loggedOutCallback = jest.fn(); @@ -85,6 +91,8 @@ describe("VaultTimeoutService", () => { authService, vaultTimeoutSettingsService, stateEventRunnerService, + taskSchedulerService, + logService, lockedCallback, loggedOutCallback, ); diff --git a/libs/common/src/services/vault-timeout/vault-timeout.service.ts b/libs/common/src/services/vault-timeout/vault-timeout.service.ts index 8d8ecd68a57..d9efef44f42 100644 --- a/libs/common/src/services/vault-timeout/vault-timeout.service.ts +++ b/libs/common/src/services/vault-timeout/vault-timeout.service.ts @@ -1,6 +1,8 @@ import { combineLatest, filter, firstValueFrom, map, switchMap, timeout } from "rxjs"; import { LogoutReason } from "@bitwarden/auth/common"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { TaskSchedulerService, ScheduledTaskNames } from "@bitwarden/common/platform/scheduling"; import { SearchService } from "../../abstractions/search.service"; import { VaultTimeoutSettingsService } from "../../abstractions/vault-timeout/vault-timeout-settings.service"; @@ -35,12 +37,19 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { private authService: AuthService, private vaultTimeoutSettingsService: VaultTimeoutSettingsService, private stateEventRunnerService: StateEventRunnerService, + private taskSchedulerService: TaskSchedulerService, + protected logService: LogService, private lockedCallback: (userId?: string) => Promise = null, private loggedOutCallback: ( logoutReason: LogoutReason, userId?: string, ) => Promise = null, - ) {} + ) { + this.taskSchedulerService.registerTaskHandler( + ScheduledTaskNames.vaultTimeoutCheckInterval, + () => this.checkVaultTimeout(), + ); + } async init(checkOnInterval: boolean) { if (this.inited) { @@ -54,10 +63,11 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { } startCheck() { - // 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.checkVaultTimeout(); - setInterval(() => this.checkVaultTimeout(), 10 * 1000); // check every 10 seconds + this.checkVaultTimeout().catch((error) => this.logService.error(error)); + this.taskSchedulerService.setInterval( + ScheduledTaskNames.vaultTimeoutCheckInterval, + 10 * 1000, // check every 10 seconds + ); } async checkVaultTimeout(): Promise { From 4edbd65faf46482cb095b5e10fc652548a03e467 Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Mon, 15 Jul 2024 08:47:30 -0700 Subject: [PATCH 29/57] [PM-3197] "Pay with PayPal" button moves on screen after loading (#10083) * hide duplicate iframe * add comment * Moved braintree styling to plugins.scss * Moved next to other braintree-sheet --------- Co-authored-by: Conner Turnbull --- apps/web/src/scss/plugins.scss | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/apps/web/src/scss/plugins.scss b/apps/web/src/scss/plugins.scss index 40ba692c463..f4aa428532c 100644 --- a/apps/web/src/scss/plugins.scss +++ b/apps/web/src/scss/plugins.scss @@ -108,6 +108,13 @@ border: none; } +// hide duplicate paypal iframe +.braintree-sheet__content--button + .braintree-sheet__button--paypal + iframe.zoid-prerender-frame.zoid-invisible { + display: none !important; +} + [data-braintree-id="upper-container"]::before { @include themify($themes) { background-color: themed("backgroundColor"); From e7b50e790ae2b684e801b452d1f0f235f23a02f1 Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Mon, 15 Jul 2024 11:56:11 -0400 Subject: [PATCH 30/57] [AC-2828] Add provider portal members page behind FF (#9949) * Add provider portal members page behind a FF * Fix reinvite issue * Import scrolling module * Add deprecations to old classes * Move members.component init to constructor * Rename new-base.people.component to base.members.component * Hide bulk reinvite when no users can be re-invited on AC members page * Rename events() to openEventsDialog() * Fix return type for members component getUsers() * Make table headers sortable * Extract row height class to ts file * Convert open methods to static methods for bulk dialogs * Rename and refactor member-dialog.component * Prevent event emission for searchControl and set filter in members component constructor * use featureFlaggedRoute rather than using FF in components * Add BaseBulkConfirmComponent for use in both web and bit-web * Add BaseBulkRemoveComponent for use in both web and bit-web * Thomas' feedback on base confirm/remove * Remaining feedback --- ...component.ts => base-members.component.ts} | 8 +- .../common/people-table-data-source.ts | 2 +- .../bulk/base-bulk-confirm.component.ts | 99 +++++++ .../bulk/base-bulk-remove.component.ts | 40 +++ .../components/bulk/bulk-status.component.ts | 4 +- .../members/members.component.html | 7 +- .../members/members.component.ts | 9 +- .../manage/bulk/bulk-confirm.component.ts | 3 + .../manage/bulk/bulk-remove.component.ts | 3 + .../add-edit-member-dialog.component.html | 68 +++++ .../add-edit-member-dialog.component.ts | 127 +++++++++ .../dialogs/bulk-confirm-dialog.component.ts | 69 +++++ .../dialogs/bulk-remove-dialog.component.ts | 49 ++++ .../providers/manage/members.component.html | 225 ++++++++++++++++ .../providers/manage/members.component.ts | 243 ++++++++++++++++++ .../providers/manage/people.component.ts | 7 +- .../manage/user-add-edit.component.ts | 3 + .../providers/providers-layout.component.html | 6 +- .../providers/providers-routing.module.ts | 25 +- .../providers/providers.module.ts | 10 + libs/common/src/enums/feature-flag.enum.ts | 2 + 21 files changed, 982 insertions(+), 27 deletions(-) rename apps/web/src/app/admin-console/common/{new-base.people.component.ts => base-members.component.ts} (97%) create mode 100644 apps/web/src/app/admin-console/organizations/members/components/bulk/base-bulk-confirm.component.ts create mode 100644 apps/web/src/app/admin-console/organizations/members/components/bulk/base-bulk-remove.component.ts create mode 100644 bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/add-edit-member-dialog.component.html create mode 100644 bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/add-edit-member-dialog.component.ts create mode 100644 bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/bulk-confirm-dialog.component.ts create mode 100644 bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/bulk-remove-dialog.component.ts create mode 100644 bitwarden_license/bit-web/src/app/admin-console/providers/manage/members.component.html create mode 100644 bitwarden_license/bit-web/src/app/admin-console/providers/manage/members.component.ts diff --git a/apps/web/src/app/admin-console/common/new-base.people.component.ts b/apps/web/src/app/admin-console/common/base-members.component.ts similarity index 97% rename from apps/web/src/app/admin-console/common/new-base.people.component.ts rename to apps/web/src/app/admin-console/common/base-members.component.ts index 90c25e840c0..c13bec78c56 100644 --- a/apps/web/src/app/admin-console/common/new-base.people.component.ts +++ b/apps/web/src/app/admin-console/common/base-members.component.ts @@ -4,7 +4,6 @@ import { FormControl } from "@angular/forms"; import { firstValueFrom, lastValueFrom, debounceTime, combineLatest, BehaviorSubject } from "rxjs"; import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe"; -import { ModalService } from "@bitwarden/angular/services/modal.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationManagementPreferencesService } from "@bitwarden/common/admin-console/abstractions/organization-management-preferences/organization-management-preferences.service"; import { @@ -35,7 +34,7 @@ export type UserViewTypes = ProviderUserUserDetailsResponse | OrganizationUserVi * This will replace BasePeopleComponent once all subclasses have been changed over to use this class. */ @Directive() -export abstract class NewBasePeopleComponent { +export abstract class BaseMembersComponent { /** * Shows a banner alerting the admin that users need to be confirmed. */ @@ -52,6 +51,10 @@ export abstract class NewBasePeopleComponent { return this.dataSource.acceptedUserCount > 0; } + get showBulkReinviteUsers(): boolean { + return this.dataSource.invitedUserCount > 0; + } + abstract userType: typeof OrganizationUserType | typeof ProviderUserType; abstract userStatusType: typeof OrganizationUserStatusType | typeof ProviderUserStatusType; @@ -77,7 +80,6 @@ export abstract class NewBasePeopleComponent { protected i18nService: I18nService, protected cryptoService: CryptoService, protected validationService: ValidationService, - protected modalService: ModalService, private logService: LogService, protected userNamePipe: UserNamePipe, protected dialogService: DialogService, diff --git a/apps/web/src/app/admin-console/common/people-table-data-source.ts b/apps/web/src/app/admin-console/common/people-table-data-source.ts index db357b4dbcd..5ce7e7bda7d 100644 --- a/apps/web/src/app/admin-console/common/people-table-data-source.ts +++ b/apps/web/src/app/admin-console/common/people-table-data-source.ts @@ -4,7 +4,7 @@ import { } from "@bitwarden/common/admin-console/enums"; import { TableDataSource } from "@bitwarden/components"; -import { StatusType, UserViewTypes } from "./new-base.people.component"; +import { StatusType, UserViewTypes } from "./base-members.component"; const MaxCheckedCount = 500; diff --git a/apps/web/src/app/admin-console/organizations/members/components/bulk/base-bulk-confirm.component.ts b/apps/web/src/app/admin-console/organizations/members/components/bulk/base-bulk-confirm.component.ts new file mode 100644 index 00000000000..8d634c38e05 --- /dev/null +++ b/apps/web/src/app/admin-console/organizations/members/components/bulk/base-bulk-confirm.component.ts @@ -0,0 +1,99 @@ +import { Directive, OnInit } from "@angular/core"; + +import { + OrganizationUserBulkPublicKeyResponse, + OrganizationUserBulkResponse, +} from "@bitwarden/common/admin-console/abstractions/organization-user/responses"; +import { ProviderUserBulkPublicKeyResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-user-bulk-public-key.response"; +import { ProviderUserBulkResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-user-bulk.response"; +import { ListResponse } from "@bitwarden/common/models/response/list.response"; +import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; + +import { BulkUserDetails } from "./bulk-status.component"; + +@Directive() +export abstract class BaseBulkConfirmComponent implements OnInit { + protected users: BulkUserDetails[]; + + protected excludedUsers: BulkUserDetails[]; + protected filteredUsers: BulkUserDetails[]; + + protected publicKeys: Map = new Map(); + protected fingerprints: Map = new Map(); + protected statuses: Map = new Map(); + + protected done = false; + protected loading = true; + protected error: string; + + protected constructor( + protected cryptoService: CryptoService, + protected i18nService: I18nService, + ) {} + + async ngOnInit() { + this.excludedUsers = this.users.filter((user) => !this.isAccepted(user)); + this.filteredUsers = this.users.filter((user) => this.isAccepted(user)); + + if (this.filteredUsers.length <= 0) { + this.done = true; + } + + const publicKeysResponse = await this.getPublicKeys(); + + for (const entry of publicKeysResponse.data) { + const publicKey = Utils.fromB64ToArray(entry.key); + const fingerprint = await this.cryptoService.getFingerprint(entry.userId, publicKey); + if (fingerprint != null) { + this.publicKeys.set(entry.id, publicKey); + this.fingerprints.set(entry.id, fingerprint.join("-")); + } + } + + this.loading = false; + } + + submit = async () => { + this.loading = true; + try { + const key = await this.getCryptoKey(); + const userIdsWithKeys: { id: string; key: string }[] = []; + + for (const user of this.filteredUsers) { + const publicKey = this.publicKeys.get(user.id); + if (publicKey == null) { + continue; + } + const encryptedKey = await this.cryptoService.rsaEncrypt(key.key, publicKey); + userIdsWithKeys.push({ + id: user.id, + key: encryptedKey.encryptedString, + }); + } + + const userBulkResponse = await this.postConfirmRequest(userIdsWithKeys); + + userBulkResponse.data.forEach((entry) => { + const error = entry.error !== "" ? entry.error : this.i18nService.t("bulkConfirmMessage"); + this.statuses.set(entry.id, error); + }); + + this.done = true; + } catch (e) { + this.error = e.message; + } + this.loading = false; + }; + + protected abstract getCryptoKey(): Promise; + protected abstract getPublicKeys(): Promise< + ListResponse + >; + protected abstract isAccepted(user: BulkUserDetails): boolean; + protected abstract postConfirmRequest( + userIdsWithKeys: { id: string; key: string }[], + ): Promise>; +} diff --git a/apps/web/src/app/admin-console/organizations/members/components/bulk/base-bulk-remove.component.ts b/apps/web/src/app/admin-console/organizations/members/components/bulk/base-bulk-remove.component.ts new file mode 100644 index 00000000000..6c736346604 --- /dev/null +++ b/apps/web/src/app/admin-console/organizations/members/components/bulk/base-bulk-remove.component.ts @@ -0,0 +1,40 @@ +import { Directive } from "@angular/core"; + +import { OrganizationUserBulkResponse } from "@bitwarden/common/admin-console/abstractions/organization-user/responses"; +import { ProviderUserBulkResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-user-bulk.response"; +import { ListResponse } from "@bitwarden/common/models/response/list.response"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; + +@Directive() +export abstract class BaseBulkRemoveComponent { + protected showNoMasterPasswordWarning: boolean; + protected statuses: Map = new Map(); + + protected done = false; + protected loading = false; + protected error: string; + + protected constructor(protected i18nService: I18nService) {} + + submit = async () => { + this.loading = true; + try { + const deleteUsersResponse = await this.deleteUsers(); + deleteUsersResponse.data.forEach((entry) => { + const error = entry.error !== "" ? entry.error : this.i18nService.t("bulkRemovedMessage"); + this.statuses.set(entry.id, error); + }); + this.done = true; + } catch (e) { + this.error = e.message; + } + + this.loading = false; + }; + + protected abstract deleteUsers(): Promise< + ListResponse + >; + + protected abstract get removeUsersWarning(): string; +} diff --git a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-status.component.ts b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-status.component.ts index ffaf27ea46d..dba6319b273 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-status.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-status.component.ts @@ -33,7 +33,7 @@ type BulkStatusDialogData = { users: Array; filteredUsers: Array; request: Promise>; - successfullMessage: string; + successfulMessage: string; }; @Component({ @@ -67,7 +67,7 @@ export class BulkStatusComponent implements OnInit { ); this.users = data.users.map((user) => { - let message = keyedErrors[user.id] ?? data.successfullMessage; + let message = keyedErrors[user.id] ?? data.successfulMessage; // eslint-disable-next-line if (!keyedFilteredUsers.hasOwnProperty(user.id)) { message = this.i18nService.t("bulkFilteredMessage"); diff --git a/apps/web/src/app/admin-console/organizations/members/members.component.html b/apps/web/src/app/admin-console/organizations/members/members.component.html index 99afe8099a6..64e4b345476 100644 --- a/apps/web/src/app/admin-console/organizations/members/members.component.html +++ b/apps/web/src/app/admin-console/organizations/members/members.component.html @@ -103,7 +103,12 @@ - diff --git a/apps/web/src/app/admin-console/organizations/members/members.component.ts b/apps/web/src/app/admin-console/organizations/members/members.component.ts index 93827539f8f..809f1e3935d 100644 --- a/apps/web/src/app/admin-console/organizations/members/members.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/members.component.ts @@ -45,7 +45,7 @@ import { Collection } from "@bitwarden/common/vault/models/domain/collection"; import { CollectionDetailsResponse } from "@bitwarden/common/vault/models/response/collection.response"; import { DialogService, SimpleDialogOptions, ToastService } from "@bitwarden/components"; -import { NewBasePeopleComponent } from "../../common/new-base.people.component"; +import { BaseMembersComponent } from "../../common/base-members.component"; import { PeopleTableDataSource } from "../../common/people-table-data-source"; import { GroupService } from "../core"; import { OrganizationUserView } from "../core/views/organization-user.view"; @@ -70,7 +70,7 @@ class MembersTableDataSource extends PeopleTableDataSource @Component({ templateUrl: "members.component.html", }) -export class MembersComponent extends NewBasePeopleComponent { +export class MembersComponent extends BaseMembersComponent { @ViewChild("resetPasswordTemplate", { read: ViewContainerRef, static: true }) resetPasswordModalRef: ViewContainerRef; @@ -94,7 +94,6 @@ export class MembersComponent extends NewBasePeopleComponent + + + {{ title | uppercase }} + {{ dialogParams.user.name }} + +
+ +

{{ "providerInviteUserDesc" | i18n }}

+
+ + + {{ "email" | i18n }} + + + {{ "inviteMultipleEmailDesc" | i18n: "20" }} + +
+
+ +

+ {{ "userType" | i18n | uppercase }} + + + +

+ + + + {{ "serviceUser" | i18n }} + + {{ "serviceUserDesc" | i18n }} + + + + {{ "providerAdmin" | i18n }} + + {{ "providerAdminDesc" | i18n }} + + +
+
+ + + +
+ +
+
+
+ diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/add-edit-member-dialog.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/add-edit-member-dialog.component.ts new file mode 100644 index 00000000000..5f88bf177ca --- /dev/null +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/add-edit-member-dialog.component.ts @@ -0,0 +1,127 @@ +import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog"; +import { Component, Inject } from "@angular/core"; +import { FormControl, FormGroup, Validators } from "@angular/forms"; + +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { ProviderUserType } from "@bitwarden/common/admin-console/enums"; +import { ProviderUserInviteRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-user-invite.request"; +import { ProviderUserUpdateRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-user-update.request"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { DialogService, ToastService } from "@bitwarden/components"; + +export type AddEditMemberDialogParams = { + providerId: string; + user?: { + id: string; + name: string; + type: ProviderUserType; + }; +}; + +export enum AddEditMemberDialogResultType { + Closed = "closed", + Deleted = "deleted", + Saved = "saved", +} + +@Component({ + templateUrl: "add-edit-member-dialog.component.html", +}) +export class AddEditMemberDialogComponent { + editing = false; + loading = true; + title: string; + + protected ResultType = AddEditMemberDialogResultType; + protected UserType = ProviderUserType; + + protected formGroup = new FormGroup({ + emails: new FormControl("", [Validators.required]), + type: new FormControl(this.dialogParams.user?.type ?? ProviderUserType.ServiceUser), + }); + + constructor( + private apiService: ApiService, + @Inject(DIALOG_DATA) protected dialogParams: AddEditMemberDialogParams, + private dialogRef: DialogRef, + private dialogService: DialogService, + private i18nService: I18nService, + private toastService: ToastService, + ) { + this.editing = this.loading = this.dialogParams.user != null; + if (this.editing) { + this.title = this.i18nService.t("editMember"); + const emailControl = this.formGroup.controls.emails; + emailControl.removeValidators(Validators.required); + emailControl.disable(); + } else { + this.title = this.i18nService.t("inviteMember"); + } + + this.loading = false; + } + + delete = async (): Promise => { + if (!this.editing) { + return; + } + + const confirmed = await this.dialogService.openSimpleDialog({ + title: this.dialogParams.user.name, + content: { key: "removeUserConfirmation" }, + type: "warning", + }); + + if (!confirmed) { + return; + } + + await this.apiService.deleteProviderUser( + this.dialogParams.providerId, + this.dialogParams.user.id, + ); + + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("removedUserId", this.dialogParams.user.name), + }); + + this.dialogRef.close(AddEditMemberDialogResultType.Deleted); + }; + + submit = async (): Promise => { + if (this.editing) { + const request = new ProviderUserUpdateRequest(); + request.type = this.formGroup.value.type; + await this.apiService.putProviderUser( + this.dialogParams.providerId, + this.dialogParams.user.id, + request, + ); + } else { + const request = new ProviderUserInviteRequest(); + request.emails = this.formGroup.value.emails.trim().split(/\s*,\s*/); + request.type = this.formGroup.value.type; + await this.apiService.postProviderUserInvite(this.dialogParams.providerId, request); + } + + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t( + this.editing ? "editedUserId" : "invitedUsers", + this.dialogParams.user?.name, + ), + }); + + this.dialogRef.close(AddEditMemberDialogResultType.Saved); + }; + + static open(dialogService: DialogService, dialogConfig: DialogConfig) { + return dialogService.open( + AddEditMemberDialogComponent, + dialogConfig, + ); + } +} diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/bulk-confirm-dialog.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/bulk-confirm-dialog.component.ts new file mode 100644 index 00000000000..d4a179091aa --- /dev/null +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/bulk-confirm-dialog.component.ts @@ -0,0 +1,69 @@ +import { DIALOG_DATA, DialogConfig } from "@angular/cdk/dialog"; +import { Component, Inject } from "@angular/core"; + +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { + OrganizationUserBulkPublicKeyResponse, + OrganizationUserBulkResponse, +} from "@bitwarden/common/admin-console/abstractions/organization-user/responses"; +import { ProviderUserStatusType } from "@bitwarden/common/admin-console/enums"; +import { ProviderUserBulkConfirmRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-user-bulk-confirm.request"; +import { ProviderUserBulkRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-user-bulk.request"; +import { ProviderUserBulkPublicKeyResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-user-bulk-public-key.response"; +import { ProviderUserBulkResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-user-bulk.response"; +import { ListResponse } from "@bitwarden/common/models/response/list.response"; +import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { DialogService } from "@bitwarden/components"; +import { BaseBulkConfirmComponent } from "@bitwarden/web-vault/app/admin-console/organizations/members/components/bulk/base-bulk-confirm.component"; +import { BulkUserDetails } from "@bitwarden/web-vault/app/admin-console/organizations/members/components/bulk/bulk-status.component"; + +type BulkConfirmDialogParams = { + providerId: string; + users: BulkUserDetails[]; +}; + +@Component({ + templateUrl: + "../../../../../../../../apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-confirm.component.html", +}) +export class BulkConfirmDialogComponent extends BaseBulkConfirmComponent { + providerId: string; + + constructor( + private apiService: ApiService, + protected cryptoService: CryptoService, + @Inject(DIALOG_DATA) protected dialogParams: BulkConfirmDialogParams, + protected i18nService: I18nService, + ) { + super(cryptoService, i18nService); + + this.providerId = dialogParams.providerId; + this.users = dialogParams.users; + } + + protected getCryptoKey = (): Promise => + this.cryptoService.getProviderKey(this.providerId); + + protected getPublicKeys = async (): Promise< + ListResponse + > => { + const request = new ProviderUserBulkRequest(this.filteredUsers.map((user) => user.id)); + return await this.apiService.postProviderUsersPublicKey(this.providerId, request); + }; + + protected isAccepted = (user: BulkUserDetails): boolean => + user.status === ProviderUserStatusType.Accepted; + + protected postConfirmRequest = async ( + userIdsWithKeys: { id: string; key: string }[], + ): Promise> => { + const request = new ProviderUserBulkConfirmRequest(userIdsWithKeys); + return await this.apiService.postProviderUserBulkConfirm(this.providerId, request); + }; + + static open(dialogService: DialogService, dialogConfig: DialogConfig) { + return dialogService.open(BulkConfirmDialogComponent, dialogConfig); + } +} diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/bulk-remove-dialog.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/bulk-remove-dialog.component.ts new file mode 100644 index 00000000000..16e64703700 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/bulk-remove-dialog.component.ts @@ -0,0 +1,49 @@ +import { DIALOG_DATA, DialogConfig } from "@angular/cdk/dialog"; +import { Component, Inject } from "@angular/core"; + +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { ProviderUserBulkRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-user-bulk.request"; +import { ProviderUserBulkResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-user-bulk.response"; +import { ListResponse } from "@bitwarden/common/models/response/list.response"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { DialogService } from "@bitwarden/components"; +import { BaseBulkRemoveComponent } from "@bitwarden/web-vault/app/admin-console/organizations/members/components/bulk/base-bulk-remove.component"; +import { BulkUserDetails } from "@bitwarden/web-vault/app/admin-console/organizations/members/components/bulk/bulk-status.component"; + +type BulkRemoveDialogParams = { + providerId: string; + users: BulkUserDetails[]; +}; + +@Component({ + templateUrl: + "../../../../../../../../apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-remove.component.html", +}) +export class BulkRemoveDialogComponent extends BaseBulkRemoveComponent { + providerId: string; + users: BulkUserDetails[]; + + constructor( + private apiService: ApiService, + @Inject(DIALOG_DATA) dialogParams: BulkRemoveDialogParams, + protected i18nService: I18nService, + ) { + super(i18nService); + + this.providerId = dialogParams.providerId; + this.users = dialogParams.users; + } + + protected deleteUsers = (): Promise> => { + const request = new ProviderUserBulkRequest(this.users.map((user) => user.id)); + return this.apiService.deleteManyProviderUsers(this.providerId, request); + }; + + protected get removeUsersWarning() { + return this.i18nService.t("removeOrgUsersConfirmation"); + } + + static open(dialogService: DialogService, dialogConfig: DialogConfig) { + return dialogService.open(BulkRemoveDialogComponent, dialogConfig); + } +} diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/members.component.html b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/members.component.html new file mode 100644 index 00000000000..66c42678442 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/members.component.html @@ -0,0 +1,225 @@ + + + + + + +
+ + + {{ "all" | i18n }} + + {{ allCount }} + + + + {{ "invited" | i18n }} + + {{ invitedCount }} + + + + {{ "needsConfirmation" | i18n }} + + {{ acceptedCount }} + + + +
+ + + + {{ "loading" | i18n }} + + + +

{{ "noMembersInList" | i18n }}

+ + + {{ "providerUsersNeedConfirmed" | i18n }} + + + + + + + + + + {{ "name" | i18n }} + {{ "role" | i18n }} + + + + + + + + + + + + + + + + +
+ +
+
+ + + {{ "invited" | i18n }} + + + {{ "needsConfirmation" | i18n }} + + + {{ "revoked" | i18n }} + +
+
+ {{ user.email }} +
+
+
+ + + {{ "providerAdmin" | i18n }} + {{ "serviceUser" | i18n }} + + + + + + + + + + + +
+
+
+
+
diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/members.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/members.component.ts new file mode 100644 index 00000000000..247297ff962 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/members.component.ts @@ -0,0 +1,243 @@ +import { DialogRef } from "@angular/cdk/dialog"; +import { Component } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { ActivatedRoute, Router } from "@angular/router"; +import { combineLatest, lastValueFrom, switchMap } from "rxjs"; +import { first } from "rxjs/operators"; + +import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe"; +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { OrganizationManagementPreferencesService } from "@bitwarden/common/admin-console/abstractions/organization-management-preferences/organization-management-preferences.service"; +import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; +import { + OrganizationUserStatusType, + ProviderUserStatusType, + ProviderUserType, +} from "@bitwarden/common/admin-console/enums"; +import { ProviderUserBulkRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-user-bulk.request"; +import { ProviderUserConfirmRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-user-confirm.request"; +import { ProviderUserUserDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-user.response"; +import { ListResponse } from "@bitwarden/common/models/response/list.response"; +import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; +import { DialogService, ToastService } from "@bitwarden/components"; +import { BaseMembersComponent } from "@bitwarden/web-vault/app/admin-console/common/base-members.component"; +import { + peopleFilter, + PeopleTableDataSource, +} from "@bitwarden/web-vault/app/admin-console/common/people-table-data-source"; +import { openEntityEventsDialog } from "@bitwarden/web-vault/app/admin-console/organizations/manage/entity-events.component"; +import { BulkStatusComponent } from "@bitwarden/web-vault/app/admin-console/organizations/members/components/bulk/bulk-status.component"; + +import { + AddEditMemberDialogComponent, + AddEditMemberDialogParams, + AddEditMemberDialogResultType, +} from "./dialogs/add-edit-member-dialog.component"; +import { BulkConfirmDialogComponent } from "./dialogs/bulk-confirm-dialog.component"; +import { BulkRemoveDialogComponent } from "./dialogs/bulk-remove-dialog.component"; + +type ProviderUser = ProviderUserUserDetailsResponse; + +class MembersTableDataSource extends PeopleTableDataSource { + protected statusType = OrganizationUserStatusType; +} + +@Component({ + templateUrl: "members.component.html", +}) +export class MembersComponent extends BaseMembersComponent { + accessEvents = false; + dataSource = new MembersTableDataSource(); + loading = true; + providerId: string; + rowHeight = 62; + rowHeightClass = `tw-h-[62px]`; + status: ProviderUserStatusType = null; + + userStatusType = ProviderUserStatusType; + userType = ProviderUserType; + + constructor( + apiService: ApiService, + cryptoService: CryptoService, + dialogService: DialogService, + i18nService: I18nService, + logService: LogService, + organizationManagementPreferencesService: OrganizationManagementPreferencesService, + toastService: ToastService, + userNamePipe: UserNamePipe, + validationService: ValidationService, + private activatedRoute: ActivatedRoute, + private providerService: ProviderService, + private router: Router, + ) { + super( + apiService, + i18nService, + cryptoService, + validationService, + logService, + userNamePipe, + dialogService, + organizationManagementPreferencesService, + toastService, + ); + + combineLatest([ + this.activatedRoute.parent.params, + this.activatedRoute.queryParams.pipe(first()), + ]) + .pipe( + switchMap(async ([urlParams, queryParams]) => { + this.searchControl.setValue(queryParams.search, { emitEvent: false }); + this.dataSource.filter = peopleFilter(queryParams.search, null); + + this.providerId = urlParams.providerId; + const provider = await this.providerService.get(this.providerId); + if (!provider || !provider.canManageUsers) { + return await this.router.navigate(["../"], { relativeTo: this.activatedRoute }); + } + this.accessEvents = provider.useEvents; + await this.load(); + + if (queryParams.viewEvents != null) { + const user = this.dataSource.data.find((user) => user.id === queryParams.viewEvents); + if (user && user.status === ProviderUserStatusType.Confirmed) { + this.openEventsDialog(user); + } + } + }), + takeUntilDestroyed(), + ) + .subscribe(); + } + + async bulkConfirm(): Promise { + if (this.actionPromise != null) { + return; + } + + const dialogRef = BulkConfirmDialogComponent.open(this.dialogService, { + data: { + providerId: this.providerId, + users: this.dataSource.getCheckedUsers(), + }, + }); + + await lastValueFrom(dialogRef.closed); + await this.load(); + } + + async bulkReinvite(): Promise { + if (this.actionPromise != null) { + return; + } + + const checkedUsers = this.dataSource.getCheckedUsers(); + const checkedInvitedUsers = checkedUsers.filter( + (user) => user.status === ProviderUserStatusType.Invited, + ); + + if (checkedInvitedUsers.length <= 0) { + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("noSelectedUsersApplicable"), + }); + return; + } + + try { + const request = this.apiService.postManyProviderUserReinvite( + this.providerId, + new ProviderUserBulkRequest(checkedInvitedUsers.map((user) => user.id)), + ); + + const dialogRef = BulkStatusComponent.open(this.dialogService, { + data: { + users: checkedUsers, + filteredUsers: checkedInvitedUsers, + request, + successfulMessage: this.i18nService.t("bulkReinviteMessage"), + }, + }); + await lastValueFrom(dialogRef.closed); + } catch (error) { + this.validationService.showError(error); + } + } + + async bulkRemove(): Promise { + if (this.actionPromise != null) { + return; + } + + const dialogRef = BulkRemoveDialogComponent.open(this.dialogService, { + data: { + providerId: this.providerId, + users: this.dataSource.getCheckedUsers(), + }, + }); + + await lastValueFrom(dialogRef.closed); + await this.load(); + } + + async confirmUser(user: ProviderUser, publicKey: Uint8Array): Promise { + const providerKey = await this.cryptoService.getProviderKey(this.providerId); + const key = await this.cryptoService.rsaEncrypt(providerKey.key, publicKey); + const request = new ProviderUserConfirmRequest(); + request.key = key.encryptedString; + await this.apiService.postProviderUserConfirm(this.providerId, user.id, request); + } + + deleteUser = (id: string): Promise => + this.apiService.deleteProviderUser(this.providerId, id); + + edit = async (user: ProviderUser | null): Promise => { + const data: AddEditMemberDialogParams = { + providerId: this.providerId, + }; + + if (user != null) { + data.user = { + id: user.id, + name: this.userNamePipe.transform(user), + type: user.type, + }; + } + + const dialogRef = AddEditMemberDialogComponent.open(this.dialogService, { + data, + }); + + const result = await lastValueFrom(dialogRef.closed); + + switch (result) { + case AddEditMemberDialogResultType.Saved: + case AddEditMemberDialogResultType.Deleted: + await this.load(); + break; + } + }; + + openEventsDialog = (user: ProviderUser): DialogRef => + openEntityEventsDialog(this.dialogService, { + data: { + name: this.userNamePipe.transform(user), + providerId: this.providerId, + entityId: user.id, + showUser: false, + entity: "user", + }, + }); + + getUsers = (): Promise> => + this.apiService.getProviderUsers(this.providerId); + + reinviteUser = (id: string): Promise => + this.apiService.postProviderUserReinvite(this.providerId, id); +} diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/people.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/people.component.ts index 564808d0055..1849809df5f 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/people.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/people.component.ts @@ -15,6 +15,7 @@ import { ProviderUserBulkRequest } from "@bitwarden/common/admin-console/models/ import { ProviderUserConfirmRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-user-confirm.request"; import { ProviderUserUserDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-user.response"; import { ListResponse } from "@bitwarden/common/models/response/list.response"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -29,6 +30,9 @@ import { BulkConfirmComponent } from "./bulk/bulk-confirm.component"; import { BulkRemoveComponent } from "./bulk/bulk-remove.component"; import { UserAddEditComponent } from "./user-add-edit.component"; +/** + * @deprecated Please use the {@link MembersComponent} instead. + */ @Component({ selector: "provider-people", templateUrl: "people.component.html", @@ -70,6 +74,7 @@ export class PeopleComponent private providerService: ProviderService, dialogService: DialogService, organizationManagementPreferencesService: OrganizationManagementPreferencesService, + private configService: ConfigService, ) { super( apiService, @@ -228,7 +233,7 @@ export class PeopleComponent users: users, filteredUsers: filteredUsers, request: response, - successfullMessage: this.i18nService.t("bulkReinviteMessage"), + successfulMessage: this.i18nService.t("bulkReinviteMessage"), }, }); await lastValueFrom(dialogRef.closed); diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/user-add-edit.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/user-add-edit.component.ts index 664e399660f..7406098ee4f 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/user-add-edit.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/user-add-edit.component.ts @@ -10,6 +10,9 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { DialogService } from "@bitwarden/components"; +/** + * @deprecated Please use the {@link MembersDialogComponent} instead. + */ @Component({ selector: "provider-user-add-edit", templateUrl: "user-add-edit.component.html", 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 9a94df877d2..5f9b3f66bc5 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 @@ -13,11 +13,7 @@ route="manage" *ngIf="showManageTab(provider)" > - + provider.canManageUsers), - ], - data: { - titleId: "people", + ...featureFlaggedRoute({ + defaultComponent: PeopleComponent, + flaggedComponent: MembersComponent, + featureFlag: FeatureFlag.AC2828_ProviderPortalMembersPage, + routeOptions: { + path: "people", + canActivate: [ + providerPermissionsGuard((provider: Provider) => provider.canManageUsers), + ], + data: { + titleId: "people", + }, }, - }, + }), { path: "events", component: EventsComponent, 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 d17c973181f..9733e91be7c 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 @@ -1,3 +1,4 @@ +import { ScrollingModule } from "@angular/cdk/scrolling"; import { CommonModule } from "@angular/common"; import { NgModule } from "@angular/core"; import { FormsModule } from "@angular/forms"; @@ -28,7 +29,11 @@ import { CreateOrganizationComponent } from "./clients/create-organization.compo import { AcceptProviderComponent } from "./manage/accept-provider.component"; import { BulkConfirmComponent } from "./manage/bulk/bulk-confirm.component"; import { BulkRemoveComponent } from "./manage/bulk/bulk-remove.component"; +import { AddEditMemberDialogComponent } from "./manage/dialogs/add-edit-member-dialog.component"; +import { BulkConfirmDialogComponent } from "./manage/dialogs/bulk-confirm-dialog.component"; +import { BulkRemoveDialogComponent } from "./manage/dialogs/bulk-remove-dialog.component"; import { EventsComponent } from "./manage/events.component"; +import { MembersComponent } from "./manage/members.component"; import { PeopleComponent } from "./manage/people.component"; import { UserAddEditComponent } from "./manage/user-add-edit.component"; import { ProvidersLayoutComponent } from "./providers-layout.component"; @@ -51,20 +56,25 @@ import { SetupComponent } from "./setup/setup.component"; PaymentMethodWarningsModule, TaxInfoComponent, DangerZoneComponent, + ScrollingModule, ], declarations: [ AcceptProviderComponent, AccountComponent, AddOrganizationComponent, BulkConfirmComponent, + BulkConfirmDialogComponent, BulkRemoveComponent, + BulkRemoveDialogComponent, ClientsComponent, CreateOrganizationComponent, EventsComponent, PeopleComponent, + MembersComponent, SetupComponent, SetupProviderComponent, UserAddEditComponent, + AddEditMemberDialogComponent, CreateClientDialogComponent, NoClientsComponent, ManageClientsComponent, diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index e9b154d2f4f..ba23b90cd22 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -23,6 +23,7 @@ export enum FeatureFlag { GroupsComponentRefactor = "groups-component-refactor", ProviderClientVaultPrivacyBanner = "ac-2833-provider-client-vault-privacy-banner", VaultBulkManagementAction = "vault-bulk-management-action", + AC2828_ProviderPortalMembersPage = "AC-2828_provider-portal-members-page", } export type AllowedFeatureFlagTypes = boolean | number | string; @@ -56,6 +57,7 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.GroupsComponentRefactor]: FALSE, [FeatureFlag.ProviderClientVaultPrivacyBanner]: FALSE, [FeatureFlag.VaultBulkManagementAction]: FALSE, + [FeatureFlag.AC2828_ProviderPortalMembersPage]: FALSE, } satisfies Record; export type DefaultFeatureFlagValueType = typeof DefaultFeatureFlagValue; From d2685e1bc5c8c55dac97da352cfe86d0d65b6258 Mon Sep 17 00:00:00 2001 From: Ike <137194738+ike-kottlowski@users.noreply.github.com> Date: Mon, 15 Jul 2024 09:04:15 -0700 Subject: [PATCH 31/57] [PM-9618] Show toast when `AuthUrl` is `null` (#10108) * Added toast message if AuthUrl is null * added toast to desktop and browser * fixed tests --- apps/browser/src/_locales/en/messages.json | 3 +++ apps/browser/src/auth/popup/two-factor.component.ts | 13 ++++++++++++- apps/desktop/src/auth/two-factor.component.ts | 12 ++++++++++++ apps/desktop/src/locales/en/messages.json | 3 +++ apps/web/src/app/auth/two-factor.component.ts | 4 +++- apps/web/src/locales/en/messages.json | 3 +++ .../auth/components/two-factor.component.spec.ts | 4 ++++ .../src/auth/components/two-factor.component.ts | 11 +++++++++++ 8 files changed, 51 insertions(+), 2 deletions(-) diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 63075053e39..bf29ff2fd26 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -3045,6 +3045,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Launch Duo and follow the steps to finish logging in." }, diff --git a/apps/browser/src/auth/popup/two-factor.component.ts b/apps/browser/src/auth/popup/two-factor.component.ts index 98363bc93cc..f3c44ca9ca2 100644 --- a/apps/browser/src/auth/popup/two-factor.component.ts +++ b/apps/browser/src/auth/popup/two-factor.component.ts @@ -25,7 +25,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { BrowserApi } from "../../platform/browser/browser-api"; import { ZonedMessageListenerService } from "../../platform/browser/zoned-message-listener.service"; @@ -62,6 +62,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent { private dialogService: DialogService, masterPasswordService: InternalMasterPasswordServiceAbstraction, accountService: AccountService, + toastService: ToastService, @Inject(WINDOW) protected win: Window, private browserMessagingApi: ZonedMessageListenerService, ) { @@ -84,6 +85,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent { configService, masterPasswordService, accountService, + toastService, ); super.onSuccessfulLogin = async () => { // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. @@ -226,6 +228,15 @@ export class TwoFactorComponent extends BaseTwoFactorComponent { } override async launchDuoFrameless() { + if (this.duoFramelessUrl === null) { + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t("duoHealthCheckResultsInNullAuthUrlError"), + }); + return; + } + const duoHandOffMessage = { title: this.i18nService.t("youSuccessfullyLoggedIn"), message: this.i18nService.t("youMayCloseThisWindow"), diff --git a/apps/desktop/src/auth/two-factor.component.ts b/apps/desktop/src/auth/two-factor.component.ts index d1b84c1fa0e..3f5e8aee19f 100644 --- a/apps/desktop/src/auth/two-factor.component.ts +++ b/apps/desktop/src/auth/two-factor.component.ts @@ -25,6 +25,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; +import { ToastService } from "@bitwarden/components"; import { TwoFactorOptionsComponent } from "./two-factor-options.component"; @@ -64,6 +65,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent { configService: ConfigService, masterPasswordService: InternalMasterPasswordServiceAbstraction, accountService: AccountService, + toastService: ToastService, @Inject(WINDOW) protected win: Window, ) { super( @@ -85,6 +87,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent { configService, masterPasswordService, accountService, + toastService, ); super.onSuccessfulLogin = async () => { // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. @@ -149,6 +152,15 @@ export class TwoFactorComponent extends BaseTwoFactorComponent { } override async launchDuoFrameless() { + if (this.duoFramelessUrl === null) { + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t("duoHealthCheckResultsInNullAuthUrlError"), + }); + return; + } + const duoHandOffMessage = { title: this.i18nService.t("youSuccessfullyLoggedIn"), message: this.i18nService.t("youMayCloseThisWindow"), diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index 51543394827..72d17baa147 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -2778,6 +2778,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Launch Duo and follow the steps to finish logging in." }, diff --git a/apps/web/src/app/auth/two-factor.component.ts b/apps/web/src/app/auth/two-factor.component.ts index b3241a92425..528ce6fda3e 100644 --- a/apps/web/src/app/auth/two-factor.component.ts +++ b/apps/web/src/app/auth/two-factor.component.ts @@ -23,7 +23,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { TwoFactorOptionsDialogResult, @@ -69,6 +69,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent implements OnDest configService: ConfigService, masterPasswordService: InternalMasterPasswordServiceAbstraction, accountService: AccountService, + toastService: ToastService, private formBuilder: FormBuilder, @Inject(WINDOW) protected win: Window, ) { @@ -91,6 +92,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent implements OnDest configService, masterPasswordService, accountService, + toastService, ); this.onSuccessfulLoginNavigate = this.goAfterLogIn; } diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 0f9e334feaa..b99f0657468 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -6210,6 +6210,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Launch Duo and follow the steps to finish logging in." }, diff --git a/libs/angular/src/auth/components/two-factor.component.spec.ts b/libs/angular/src/auth/components/two-factor.component.spec.ts index 0eb248f6d9d..3325f3bc32d 100644 --- a/libs/angular/src/auth/components/two-factor.component.spec.ts +++ b/libs/angular/src/auth/components/two-factor.component.spec.ts @@ -32,6 +32,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; import { UserId } from "@bitwarden/common/types/guid"; +import { ToastService } from "@bitwarden/components"; import { TwoFactorComponent } from "./two-factor.component"; @@ -71,6 +72,7 @@ describe("TwoFactorComponent", () => { let mockConfigService: MockProxy; let mockMasterPasswordService: FakeMasterPasswordService; let mockAccountService: FakeAccountService; + let mockToastService: MockProxy; let mockUserDecryptionOpts: { noMasterPassword: UserDecryptionOptions; @@ -102,6 +104,7 @@ describe("TwoFactorComponent", () => { mockSsoLoginService = mock(); mockConfigService = mock(); mockAccountService = mockAccountServiceWith(userId); + mockToastService = mock(); mockMasterPasswordService = new FakeMasterPasswordService(); mockUserDecryptionOpts = { @@ -182,6 +185,7 @@ describe("TwoFactorComponent", () => { { provide: ConfigService, useValue: mockConfigService }, { provide: InternalMasterPasswordServiceAbstraction, useValue: mockMasterPasswordService }, { provide: AccountService, useValue: mockAccountService }, + { provide: ToastService, useValue: mockToastService }, ], }); diff --git a/libs/angular/src/auth/components/two-factor.component.ts b/libs/angular/src/auth/components/two-factor.component.ts index d08e9a0a2ef..8c849db6c63 100644 --- a/libs/angular/src/auth/components/two-factor.component.ts +++ b/libs/angular/src/auth/components/two-factor.component.ts @@ -32,6 +32,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; +import { ToastService } from "@bitwarden/components"; import { CaptchaProtectedComponent } from "./captcha-protected.component"; @@ -94,6 +95,7 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI protected configService: ConfigService, protected masterPasswordService: InternalMasterPasswordServiceAbstraction, protected accountService: AccountService, + protected toastService: ToastService, ) { super(environmentService, i18nService, platformUtilsService); this.webAuthnSupported = this.platformUtilsService.supportsWebAuthn(win); @@ -474,6 +476,15 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI } async launchDuoFrameless() { + if (this.duoFramelessUrl === null) { + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t("duoHealthCheckResultsInNullAuthUrlError"), + }); + return; + } + this.platformUtilsService.launchUri(this.duoFramelessUrl); } } From 7ed143dc62d14ee0711a87bf4a7b5baca7cec012 Mon Sep 17 00:00:00 2001 From: Bitwarden DevOps <106330231+bitwarden-devops-bot@users.noreply.github.com> Date: Mon, 15 Jul 2024 10:51:19 -0600 Subject: [PATCH 32/57] Bumped client version(s) (#10122) --- apps/web/package.json | 2 +- package-lock.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/web/package.json b/apps/web/package.json index 11bf27b4e39..b75d9eac4c2 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@bitwarden/web-vault", - "version": "2024.7.0", + "version": "2024.7.1", "scripts": { "build:oss": "webpack", "build:bit": "webpack -c ../../bitwarden_license/bit-web/webpack.config.js", diff --git a/package-lock.json b/package-lock.json index 6888df5e686..b0681640f98 100644 --- a/package-lock.json +++ b/package-lock.json @@ -249,7 +249,7 @@ }, "apps/web": { "name": "@bitwarden/web-vault", - "version": "2024.7.0" + "version": "2024.7.1" }, "libs/admin-console": { "name": "@bitwarden/admin-console", From e973d72b0170a9fe40a7f4a0ac383c34bc84e2f0 Mon Sep 17 00:00:00 2001 From: Cesar Gonzalez Date: Mon, 15 Jul 2024 11:57:21 -0500 Subject: [PATCH 33/57] [PM-5189] Fix issues with inline menu rendering in iframes and SPA websites (#8431) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [PM-8027] Working through jest tests for the InlineMenuFieldQualificationService * [PM-8027] Finalization of Jest test for the implementation * [PM-5189] Fixing existing jest tests before undergoing larger scale rework of tests * [PM-5189] Implementing jest tests for the OverlayBackground * [PM-5189] Working through jest tests for OverlayBackground * [PM-5189] Working through jest tests for OverlayBackground * [PM-5189] Reworking how we handle updating ciphers on unlock and updating reference to auth status to use observable * [PM-5189] Fixing issue with how we remove the inline menu when a field is populated * [PM-5189] Fixing issue with programmatic redirection of the inlne menu * [PM-5189] Fixing issue with programmatic redirection of the inlne menu * [PM-5189] Adjusting how we handle fade out of the inline menu element * [PM-5189] Implementing jest tests for the OverlayBackground * [PM-5189] Implementing jest tests for the OverlayBackground * [PM-5189] Implementing jest tests for the OverlayBackground * [PM-5189] Implementing jest tests for the OverlayBackground * [PM-5189] Implementing jest tests for the OverlayBackground * [PM-5189] Implementing jest tests for the OverlayBackground * [PM-5189] Implementing jest tests for the OverlayBackground * [PM-5189] Implementing jest tests for the OverlayBackground * [PM-5189] Implementing jest tests for the OverlayBackground * [PM-5189] Implementing jest tests for the OverlayBackground * [PM-5189] Fixing a weird side issue that appears when a frame within the page triggers a reposition after the inline menu has been built * [PM-5189] Fixing a weird side issue that appears when a frame within the page triggers a reposition after the inline menu has been built * [PM-5189] Fixing a weird side issue that appears when a frame within the page triggers a reposition after the inline menu has been built * [PM-8027] Fixing a typo * [PM-8027] Incorporating a feature flag to allow us to fallback to the basic inline menu fielld qualification method if needed * [PM-8027] Incorporating a feature flag to allow us to fallback to the basic inline menu fielld qualification method if needed * [PM-5189] Refactoring implementation * [PM-5189] Refactoring implementation * [PM-5189] Refactoring implementation * [PM-5189] Refactoring implementation * [PM-5189] Refactoring implementation * [PM-5189] Refactoring implementation * [PM-5189] Adding jest tests for added methods in AutofillInit * [PM-5189] Refactoring implementation * [PM-5189] Implementing jest tests for the CollectAutofillContentService * [PM-5189] Implementing jest tests for the AutofillOverlayContentService * [PM-5189] Working through jest tests for the AutofillOverlayContentService * [PM-5189] Working through jest tests for the AutofillOverlayContentService * [PM-5189] Working through jest tests for the AutofillOverlayContentService * [PM-5189] Working through jest tests for the AutofillOverlayContentService * [PM-5189] Working through jest tests for the AutofillOverlayContentService * [PM-5189] Implementing jest tests for AutofillInlineMenuIframeServce * [PM-5189] Fixing a typo * [PM-5189] Fixing a typo * [PM-5189] Correcting typing information * [PM-5189] Fixing some typos * [PM-5189] Refactoring implementation * [PM-5189] Refactoring implementation * [PM-5189] Refactoring implementation * [PM-5189] Refactoring implementation0 * [PM-5189] Refactoring implementation * [PM-5189] Refactoring implementation * [PM-5189] Implementing jest tests for AutofillInlineMenuContentService * [PM-5189] Implementing jest tests for AutofillInlineMenuContentService * [PM-5189] Implementing jest tests for AutofillInlineMenuContentService * [PM-5189] Implementing jest tests for AutofillInlineMenuContentService * [PM-5189] Implementing jest tests for AutofillInlineMenuContentService * [PM-5189] Implementing jest tests for AutofillInlineMenuContentService * [PM-5189] Implementing jest tests for AutofillInlineMenuContentService * [PM-5189] Fixing an issue found with iframe service * [PM-5189] Refactoring implementation * [PM-5189] Refactoring implementation * [PM-5189] Refactoring implementation * [PM-5189] Refactoring implementation * [PM-5189] Refactoring implementation * [PM-5189] Removing TODO message * [PM-5189] Increasing the time we delay the closure of the inline menu * [PM-5189] Fixing an issue with how we handle closing the inline menu after a programmtic redirection * [PM-5189] Removing unnecessary property * [PM-5189] Removing unnecessary property * [PM-5189] Fixing an issue with how scroll events trigger a reposition of the inline menu when the field is not focused; * [PM-5189] Implementing a set threshold for the maximum depth for which we are willing to calculate sub frame offsets * [PM-5189] Implementing a set threshold for the maximum depth for which we are willing to calculate sub frame offsets * [PM-5189] Implementing a set threshold for the maximum depth for which we are willing to calculate sub frame offsets * [PM-5189] Implementing a set threshold for the maximum depth for which we are willing to calculate sub frame offsets * [PM-5189] Fixing jest tests * [PM-5189] Implementing a methodology for triggering subframe updates from layout-shift * [PM-5189] Implementing a methodology for triggering subframe updates from layout-shift * [PM-8027] Fixing issue with username fields not qualifyng as a valid login field if a viewable password field is not present * [PM-8027] Fixing issue with username fields not qualifyng as a valid login field if a viewable password field is not present * [PM-8027] Fixing an issue where a field that has no form and no visible password fields should be qualified if a single password field exists in the page * [PM-8027] Fixing an issue where a field that has no form and no visible password fields should be qualified if a single password field exists in the page * [PM-5189] Implementing a methodology for triggering subframe updates from layout-shift * [PM-5189] Implementing a methodology for triggering subframe updates from layout-shift * [PM-8869] Autofill features broken on Safari * [PM-8869] Autofill features broken on Safari * [PM-5189] Working through subFrameRecalculation approach * [PM-5189] Fixing an issue found within Safari * [PM-8027] Reverting flag from a fallback flag to an enhancement feature flag * [PM-8027] Fixing jest tests * [PM-5189] Reworking how we handle updating ciphers within nested sub frames * [PM-5189] Reworking how we handle updating ciphers within nested sub frames * [PM-5189] Reworking how we handle updating ciphers within nested sub frames * [PM-5189] Reworking how we handle updating ciphers within nested sub frames * [PM-5189] Fixing issue found in Safari with how the inline menu is re-positioned * [PM-5189] Fixing issue found in Safari with how the inline menu is re-positioned * [PM-5189] Fixing issue found in Safari with how the inline menu is re-positioned * [PM-5189] Fixing jest tests * [PM-5189] Fixing jest tests * [PM-5189] Refining how we handle fading in the inline menu elements * [PM-5189] Refining how we handle fading in the inline menu elements * [PM-5189] Refining how we handle fading in the inline menu elements * [PM-5189] Reworking how we handle updating the inline menu position * [PM-5189] Reworking how we handle updating the inline menu position * [PM-5189] Reworking how we handle updating the inline menu position * [PM-5189] Reworking how we handle updating the inline menu position * [PM-5189] Reworking how we handle updating the inline menu position * [PM-5189] Reworking how we handle updating the inline menu position * [PM-5189] Reworking how we handle updating the inline menu position * [PM-5189] Working through content script port improvement * [PM-5189] Working through content script port improvement * [PM-5189] Working through content script port improvement * [PM-5189] Working through content script port improvement * [PM-5189] Working through content script port improvement * [PM-5189] Working through content script port improvement * [PM-5189] Working through content script port improvement * [PM-5189] Working through content script port improvement * [PM-5189] Working through content script port improvement * Revert "[PM-5189] Working through content script port improvement" This reverts commit 857008413f768db6595203c6af890cebca9e2cf6. * Revert "[PM-5189] Working through content script port improvement" This reverts commit f219d7107085cdf5330a0b9df41c2e966938ef3c. * Revert "[PM-5189] Working through content script port improvement" This reverts commit f389263b644adb1b7886d2b397e223f0cbd374a4. * Revert "[PM-5189] Working through content script port improvement" This reverts commit 8a48e576e13e94b8003d57e610356af73e378a75. * Revert "[PM-5189] Working through content script port improvement" This reverts commit e30a1ebc5d2f36b8fbf3e92b13a6f7426e366e44. * Revert "[PM-5189] Working through content script port improvement" This reverts commit da357f46b3e8d062a37f420a47cadaddb35eecc2. * [PM-5189] Reverting content script port rework * [PM-5189] Fixing jest tests for AutofillOverlayContentService * [PM-5189] Adding documentation for the AutofillOverlayContentService * [PM-5189] Working through jest tests for OverlayBackground and refining repositioning delays * [PM-5189] Working through jest tests for OverlayBackground and refining repositioning delays * [PM-5189] Throttling how often sub frame calculations can be triggered from the focus in listener * [PM-5189] Finalizing jest tests for AutofillOverlayContentService * [PM-5189] Finalizing jest tests for AutofillOverlayContentService * [PM-5189] Finalizing jest tests for AutofillOverlayContentService * [PM-5189] Removing custom debounce method that is unused * [PM-5189] Removing custom debounce method that is unused * [PM-2857] Reworking how we handle invalidating cache when a tab chagne has occurred * [PM-5189] Fixing issues found within code review behind how we position elements * [PM-5189] Fixing issues found within code review behind how we position elements * [PM-5189] Fixing issues found within code review behind how we position elements * [PM-5189] Fixing issues found within code review behind how we position elements * [PM-5189] Fixing issues found within code review behind how we position elements * [PM-5189] Fixing issues found within code review behind how we position elements * [PM-5189] Adding jest tests for OverlayBackground methods * [PM-5189] Adding jest tests for OverlayContentService methods * [PM-5189] Working through further issues on positioning of inline menu * [PM-5189] Working through further issues on positioning of inline menu * [PM-5189] Working through further issues on positioning of inline menu * [PM-5189] Working through further issues on positioning of inline menu * [PM-5189] Working through jest tests for OverlayBackground and refining repositioning delays * [PM-5189] Working through jest tests for OverlayBackground and refining repositioning delays * [PM-5189] Working through jest tests for OverlayBackground and refining repositioning delays * [PM-5189] Working through jest tests for OverlayBackground and refining repositioning delays * [PM-5189] Working through jest tests for OverlayBackground and refining repositioning delays * [PM-5189] Fixing an issue found when switching between open windows * [PM-5189] Fixing an issue found when switching between open windows * [PM-5189] Removing throttle from resize listeners within the content script * [PM-5189] Removing throttle from resize listeners within the content script * [PM-5189] Fixing issue within Safari relating to repositioning elements from outer frame * [PM-5189] Fixing issue within Safari relating to repositioning elements from outer frame * [PM-5189] Fixing issue within Safari relating to repositioning elements from outer frame * [PM-5189] Adding some documentation and adjust jest test for util method * [PM-5189] Reverting naming structure for OverlayBackground method * [PM-5189] Fixing a missed promise reference within OverlayBackground * [PM-5189] Removing throttle from resize listeners within the content script * Revert "[PM-5189] Removing throttle from resize listeners within the content script" This reverts commit 62cf0f8f24dcccd21883f07b78855b26660cccb8. * [PM-5189] Re-adding throttle and reducing delay * [PM-5189] Fixing an issue with onButton click settings not being respected when a reposition event occurs * [PM-5189] Adding a missing test to OverlayBackground * [PM-5189] Fixing an issue where we trigger a blur event when the inline menu is hovered, but the page takes focus away * [PM-9342] Inline menu does not show on username field for a form that has a password field with an invalid autocomplete value * [PM-9342] Incorporating logic to handle multiple autocomplete values within a captured set of page details * [PM-9342] Incorporating logic to handle multiple autocomplete values within a captured set of page details * [PM-9342] Changing logic for how we identify new password fields to reflect a more assertive qualification * [PM-9342] Adding feedback from code review * [PM-5189] Fixing an issue where the port key for an inline menu element could potentially be undefined if the window focus changes too quickly * [PM-5189] Fixing an issue where we can potentially show the inline menu incorrectly after a user switches account * [PM-5189] Fixing an issue where we can potentially show the inline menu incorrectly after a user switches account * [PM-5189] Fixing an issue where we can potentially show the inline menu incorrectly after a user switches account * [PM-5189] Fixing an issue where we can potentially show the inline menu incorrectly after a user switches account * [PM-9267] Implement feature flag for inline menu re-architecture (#9845) * [PM-9267] Implement Feature Flag for Inline Menu Re-Architecture * [PM-9267] Incorporating legacy OverlayBackground implementation * [PM-9267] Incorporating legacy overlay content scripts * [PM-9267] Incorporating legacy overlay content scripts * [PM-9267] Incorporating legacy overlay content scripts * [PM-9267] Incorporating legacy overlay content scripts * [PM-9267] Finalizing feature flag implementation * [PM-9267] Finalizing feature flag implementation * [PM-9267] Finalizing feature flag implementation * [PM-9267] Finalizing feature flag implementation * [PM-9267] Finalizing feature flag implementation * [PM-9267] Finalizing feature flag implementation * [PM-9267] Finalizing feature flag implementation * [PM-9267] Finalizing feature flag implementation * [PM-9267] Adjusting naming convention for page files * [PM-9267] Adjusting naming convention for page files * [PM-5189] Fixing an issue where we can potentially show the inline menu incorrectly after a user switches account * PM-4950 - Fix hint and verify delete components that had the data in the wrong place (#9877) * PM-4661: Add passkey.username as item.username (#9756) * Add incoming passkey.username as item.username * Driveby fix, was sending wrong username * added username to new-cipher too * Guarded the if-block * Update apps/browser/src/vault/popup/components/vault/add-edit.component.ts Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com> * Fixed broken test * fixed username on existing ciphers --------- Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com> * PM-4878: Add passkey information to items when signing in (#9835) * Added username to subtitle * Added subName to cipher * Moved subName to component * Update apps/browser/src/vault/popup/components/fido2/fido2-cipher-row.component.ts Co-authored-by: SmithThe4th * Fixed double code and added comment * Added changeDetection: ChangeDetectionStrategy.OnPush as per review --------- Co-authored-by: SmithThe4th * [AC-2791] Members page - finish component library refactors (#9727) * Replace PlatformUtilsService with ToastService * Remove unneeded templates * Implement table filtering function * Move member-only methods from base class to subclass * Move utility functions inside new MemberTableDataSource * Rename PeopleComponent to MembersComponent * [deps] Platform: Update angular-cli monorepo to v16.2.14 (#9380) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * [PM-8789] Move desktop_native into subcrate (#9682) * Move desktop_native into subcrate * Add publish = false to crates * [PM-6394] remove policy evaluator cache (#9807) * [PM-9364] Copy for Aggregate auto-scaling invoices for Teams and Enterprise customers (#9875) * Change the seat adjustment message * Move changes from en_GB file to en file * revert changes in en_GB file * Add feature flag to the change * use user verification as a part of key rotation (#9722) * Add the ability for custom validation logic to be injected into `UserVerificationDialogComponent` (#8770) * Introduce `verificationType` * Update template to use `verificationType` * Implement a path for `verificationType = 'custom'` * Delete `clientSideOnlyVerification` * Update `EnrollMasterPasswordResetComponent` to include a server-side hash check * Better describe the custom scenerio through comments * Add an example of the custom verficiation scenerio * Move execution of verification function into try/catch * Migrate existing uses of `clientSideOnlyVerification` * Use generic type option instead of casting * Change "given" to "determined" in a comment * Restructure the `org-redirect` guard to be Angular 17+ compliant (#9552) * Document the `org-redirect` guard in code * Make assertions about the way the `org-redirect` guard should behave * Restructure the `org-redirect` guard to be Angular 17+ compliant * Convert data parameter to function parameter * Convert a data parameter to a function parameter that was missed * Pass redirect function to default organization route * don't initialize kdf with validators, do it on first set (#9754) * add testids for attachments (#9892) * Bug fix - error toast in 2fa (#9623) * Bug fix - error toast in 2fa * Bug fix - Yubikey code obscured * 2FA error fix * Restructure the `is-paid-org` guard to be Angular 17+ compliant (#9598) * Document that `is-paid-org` guard in code * Remove unused `MessagingService` dependency * Make assertions about the way the is-paid-org guard should behave * Restructure the `is-paid-org` guard to be Angular 17+ compliant * Random commit to get the build job moving * Undo previous commit * Bumped client version(s) (#9895) * [PM-9344] Clarify accepted user state (#9861) * Prefer `Needs confirmation` to `Accepted` display status This emphasizes that action is still required to complete setup. * Remove unused message * Bumped client version(s) (#9906) * Revert "Bumped client version(s) (#9906)" (#9907) This reverts commit 78c28297938eda53e7731fdf9f63d7baa7068d0d. * fix duo subscriptions and org vs individual duo setup (#9859) * [PM-5024] Migrate tax-info component (#9872) * Changes for the tax info migration * Return for invalid formgroup * Restructure the `org-permissions` guard to be Angular 17+ compliant (#9631) * Document the `org-permissions` guard in code * Restructure the `org-permissions` guard to be Angular 17+ compliant * Update the `org-permissions` guard to use `ToastService` * Simplify callback function sigantures * Remove unused test object * Fix updated route from merge * Restructure the `provider-permissions` guard to be Angular 17+ compliant (#9609) * Document the `provider-permissions` guard in code * Restructure the `provider-permissions` guard to be Angular 17+ compliant * [deps] Platform: Update @types/argon2-browser to v1.18.4 (#8180) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Bumped client version(s) (#9914) * [PM-7162] Cipher Form - Item Details (#9758) * [PM-7162] Fix weird angular error regarding disabled component bit-select * [PM-7162] Introduce CipherFormConfigService and related types * [PM-7162] Introduce CipherFormService * [PM-7162] Introduce the Item Details section component and the CipherFormContainer interface * [PM-7162] Introduce the CipherForm component * [PM-7162] Add strongly typed QueryParams to the add-edit-v2.component * [PM-7162] Export CipherForm from Vault Lib * [PM-7162] Use the CipherForm in Browser AddEditV2 * [PM-7162] Introduce CipherForm storybook * [PM-7162] Remove VaultPopupListFilterService dependency from NewItemDropDownV2 component * [PM-7162] Add support for content projection of attachment button * [PM-7162] Fix typo * [PM-7162] Cipher form service cleanup * [PM-7162] Move readonly collection notice to bit-hint * [PM-7162] Refactor CipherFormConfig type to enforce required properties with Typescript * [PM-7162] Fix storybook after config changes * [PM-7162] Use new add-edit component for clone route * [deps]: Update @yao-pkg/pkg to ^5.12.0 (#9820) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Autosync the updated translations (#9922) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> * Autosync the updated translations (#9923) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> * Autosync the updated translations (#9924) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> * [AC-2830] Unable to create a free organization (#9917) * Resolve the issue free org creation * Check that the taxForm is touched * [PM-7162] Fix broken getter when original cipher is null (#9927) * [PM-8525] Edit Card (#9901) * initial add of card details section * add card number * update card brand when the card number changes * add year and month fields * add security code field * hide number and security code by default * add `id` for all form fields * update select options to match existing options * make year input numerical * only display card details for card ciphers * use style to set input height * handle numerical values for year * update heading when a brand is available * remove unused ref * use cardview types for the form * fix numerical input type * disable card details when in partial-edit mode * remove hardcoded height * update types for formBuilder * [PM-9440] Fix: handle undefined value in migration 66 (#9908) * fix: handle undefined value in migration 66 * fix: the if-statement was typo * Rename "encryptionAlgorithm" to "hashAlgorithmForEncryption" for clarity (#9891) * [PM-7972] Account switching integration with "remember email" functionality (#9750) * add account switching logic to login email service * enforce boolean and fix desktop account switcher order * [PM-9442] Add tests for undefined state values and proper emulation of ElectronStorageService in tests (#9910) * fix: handle undefined value in migration 66 * fix: the if-statement was typo * feat: duplicate error behavior in fake storage service * feat: fix all migrations that were setting undefined values * feat: add test for disabled fingrint in migration 66 * fix: default single user state saving undefined value to state * revert: awaiting floating promise gonna fix this in a separate PR * Revert "feat: fix all migrations that were setting undefined values" This reverts commit 034713256cee9a8e164295c88157fe33d8372c81. * feat: automatically convert save to remove * Revert "fix: default single user state saving undefined value to state" This reverts commit 6c36da6ba52f6886d0de2b502b3aaff7f122c3a7. * [AC-2805] Consolidated Billing UI Updates (#9893) * Add empty state for invoices * Make cards on create client dialog tabbable * Add space in $ / month per member * Mute text, remove (Monthly) and right align menu on clients table * Made used seats account for all users and fixed column sort for used/remaining * Resize pricing cards * Rename assignedSeats to occupiedSeats * [PM-9460][deps] Tools: Update electron to v31 (#9921) * [deps] Tools: Update electron to v31 * Bump version in electron-builder --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Daniel James Smith * [AC-1452] Restrict access to 'Organization Info' and 'Two-Step Login' settings pages with a permission check (#9483) * Guard Organization Info route - Owners only * Guard TwoFactor route - Owners only and Organization must be able to use 2FA * Update guards to use function syntax --------- Co-authored-by: Addison Beck * [PM-9437] Use CollectionAccessDetailsResponse type now that is always the type returned from the API (#9951) * Add required env variables to desktop native build script (#9869) * [AC-2676] Remove paging logic from GroupsComponent (#9705) * remove infinite scroll, use virtual scroll instead * use TableDataSource for search * allow sorting by name * replacing PlatformUtilsService.showToast with ToastService * misc FIXMEs * [PM-9441] Catch and log exceptions during migration (#9905) * feat: catch and log exceptions during migration * Revert "feat: catch and log exceptions during migration" This reverts commit d68733b7e58120298974b350e496bb3e0c9af0d2. * feat: use log service to log migration errors * Autosync the updated translations (#9972) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> * Autosync the updated translations (#9973) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> * Updated codeowners for new design system team (#9913) * Updated codeowners for new design system team. * Moved Angular and Bootstrap dependencies * Moved additional dependencies. * Updated ownership Co-authored-by: Will Martin --------- Co-authored-by: Will Martin * [SM-1016] Fix new access token dialog (#9918) * swap to bit-dialog title & subtitle * remove dialogRef.disableClose & use toastService * Add shared two-factor-options component (#9767) * Communicate the upcoming client vault privacy changes to MSPs (#9994) * Add a banner notification to the provider portal * Feature flag the banner * Move banner copy to messages.json * Allow for dismissing the banner * Auth/PM-7321 - Registration with Email Verification - Registration Finish Component Implementation (#9653) * PM-7321 - Temp add input password * PM-7321 - update input password based on latest PR changes to test. * PM-7321 - Progress on testing input password component + RegistrationFinishComponent checks * PM-7321 - more progress on registration finish. * PM-7321 - Wire up RegistrationFinishRequest model + AccountApiService abstraction + implementation changes for new method. * PM-7321 - WIP Registration Finish - wiring up request building and API call on submit. * PM-7321 - WIP registratin finish * PM-7321 - WIP on creating registration-finish service + web override to add org invite handling * PM-7321 - (1) Move web-registration-finish svc to web (2) Wire up exports (3) wire up RegistrationFinishComponent to call registration finish service * PM-7321 - Get CLI building * PM-7321 - Move all finish registration service and content to registration-finish feature folder. * PM-7321 - Fix RegistrationFinishService config * PM-7321 - RegistrationFinishComponent- handlePasswordFormSubmit - error handling WIP * PM-7321 - InputPasswordComp - Update to accept masterPasswordPolicyOptions as input instead of retrieving it as parent components in different scenarios will need to retrieve the policies differently (e.g., orgInvite token in registration vs direct call via org id post SSO on set password) * PM-7321 - Registration Finish - Add web specific logic for retrieving master password policies and passing them into the input password component. * PM-7321 - Registration Start - Send email via query param to registration finish page so it can create masterKey * PM-7321 - InputPassword comp - (1) Add loading input (2) Add email validation to submit logic. * PM-7321 - Registration Finish - Add submitting state and pass into input password so that the rest of the registration process keeps the child form disabled. * PM-7321 - Registration Finish - use validation service for error handling. * PM-7321 - All register routes must be dynamic and change if the feature flag changes. * PM-7321 - Test registration finish services. * PM-7321 - RegisterRouteService - Add comment documenting why the service exists. * PM-7321 - Add missing input password translations to browser & desktop * PM-7321 - WebRegistrationFinishSvc - apply PR feedback * [deps] Autofill: Update rimraf to v5.0.8 (#10008) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * [PM-9318] Fix username on protonpass import (#9889) * Fix username field used for ProtonPass import ProtonPass has changed their export format and userName is not itemEmail * Import additional field itemUsername --------- Co-authored-by: Daniel James Smith * [PM-8943] Update QRious script initialization in Authenticator two-factor provider (#9926) * create onload() for qrious as well as error messaging if QR code cannot be displayed * button and message updates and formpromise removal * load QR script async * rename and reorder methods * Delete Unused Bits of StateService (#9858) * Delete Unused Bits of StateService * Fix Tests * remove getBgService for auth request service (#10020) --------- Co-authored-by: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com> Co-authored-by: Anders Åberg Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com> Co-authored-by: SmithThe4th Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Daniel García Co-authored-by: ✨ Audrey ✨ Co-authored-by: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Co-authored-by: Jake Fink Co-authored-by: Addison Beck Co-authored-by: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> Co-authored-by: vinith-kovan <156108204+vinith-kovan@users.noreply.github.com> Co-authored-by: Bitwarden DevOps <106330231+bitwarden-devops-bot@users.noreply.github.com> Co-authored-by: Matt Gibson Co-authored-by: Opeyemi Co-authored-by: Shane Melton Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Andreas Coroiu Co-authored-by: Bernd Schoolmann Co-authored-by: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Co-authored-by: Daniel James Smith Co-authored-by: Rui Tomé <108268980+r-tome@users.noreply.github.com> Co-authored-by: Addison Beck Co-authored-by: Todd Martin <106564991+trmartin4@users.noreply.github.com> Co-authored-by: Will Martin Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> Co-authored-by: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> Co-authored-by: Ike <137194738+ike-kottlowski@users.noreply.github.com> --------- Co-authored-by: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com> Co-authored-by: Anders Åberg Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com> Co-authored-by: SmithThe4th Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Daniel García Co-authored-by: ✨ Audrey ✨ Co-authored-by: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Co-authored-by: Jake Fink Co-authored-by: Addison Beck Co-authored-by: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> Co-authored-by: vinith-kovan <156108204+vinith-kovan@users.noreply.github.com> Co-authored-by: Bitwarden DevOps <106330231+bitwarden-devops-bot@users.noreply.github.com> Co-authored-by: Matt Gibson Co-authored-by: Opeyemi Co-authored-by: Shane Melton Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Andreas Coroiu Co-authored-by: Bernd Schoolmann Co-authored-by: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Co-authored-by: Daniel James Smith Co-authored-by: Rui Tomé <108268980+r-tome@users.noreply.github.com> Co-authored-by: Addison Beck Co-authored-by: Todd Martin <106564991+trmartin4@users.noreply.github.com> Co-authored-by: Will Martin Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> Co-authored-by: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> Co-authored-by: Ike <137194738+ike-kottlowski@users.noreply.github.com> --- .../abstractions/overlay.background.ts | 160 +- .../background/notification.background.ts | 6 +- .../background/overlay.background.spec.ts | 3161 ++++++++++------- .../autofill/background/overlay.background.ts | 1274 +++++-- .../background/tabs.background.spec.ts | 15 +- .../autofill/background/tabs.background.ts | 9 +- .../content/abstractions/autofill-init.ts | 36 +- .../autofill/content/autofill-init.spec.ts | 363 +- .../src/autofill/content/autofill-init.ts | 197 +- .../content/bootstrap-autofill-overlay.ts | 18 +- .../overlay.background.deprecated.ts | 124 + .../overlay.background.deprecated.spec.ts | 1463 ++++++++ .../overlay.background.deprecated.ts | 798 +++++ .../abstractions/autofill-init.deprecated.ts | 41 + .../content/autofill-init.deprecated.spec.ts | 604 ++++ .../content/autofill-init.deprecated.ts | 310 ++ .../bootstrap-legacy-autofill-overlay.ts | 14 + .../autofill-overlay-button.deprecated.ts} | 0 ...fill-overlay-iframe.service.deprecated.ts} | 0 .../autofill-overlay-list.deprecated.ts} | 2 +- ...tofill-overlay-page-element.deprecated.ts} | 4 +- ...ay-iframe.service.deprecated.spec.ts.snap} | 2 +- ...-overlay-button-iframe.deprecated.spec.ts} | 2 +- ...ofill-overlay-button-iframe.deprecated.ts} | 4 +- ...overlay-iframe-element.deprecated.spec.ts} | 6 +- ...fill-overlay-iframe-element.deprecated.ts} | 2 +- ...overlay-iframe.service.deprecated.spec.ts} | 10 +- ...fill-overlay-iframe.service.deprecated.ts} | 4 +- ...ll-overlay-list-iframe.deprecated.spec.ts} | 2 +- ...utofill-overlay-list-iframe.deprecated.ts} | 4 +- ...ll-overlay-button.deprecated.spec.ts.snap} | 0 ...utofill-overlay-button.deprecated.spec.ts} | 33 +- .../autofill-overlay-button.deprecated.ts} | 8 +- ...trap-autofill-overlay-button.deprecated.ts | 9 + .../overlay/pages/button/legacy-button.html} | 2 +- .../overlay/pages/button/legacy-button.scss | 36 + ...fill-overlay-list.deprecated.spec.ts.snap} | 0 .../autofill-overlay-list.deprecated.spec.ts} | 66 +- .../list/autofill-overlay-list.deprecated.ts} | 12 +- ...tstrap-autofill-overlay-list.deprecated.ts | 9 + .../overlay/pages/list/legacy-list.html | 12 + .../overlay/pages/list/legacy-list.scss | 293 ++ ...l-overlay-page-element.deprecated.spec.ts} | 11 +- ...tofill-overlay-page-element.deprecated.ts} | 4 +- .../autofill-overlay-content.service.ts | 37 + ...overlay-content.service.deprecated.spec.ts | 1743 +++++++++ ...fill-overlay-content.service.deprecated.ts | 1133 ++++++ .../autofill/enums/autofill-overlay.enum.ts | 22 + ...ll-port.enums.ts => autofill-port.enum.ts} | 0 .../fido2/background/fido2.background.ts | 6 +- .../autofill-inline-menu-button.ts | 33 + .../autofill-inline-menu-container.ts | 31 + .../autofill-inline-menu-content.service.ts | 13 + .../autofill-inline-menu-iframe.service.ts | 30 + .../abstractions/autofill-inline-menu-list.ts | 30 + .../autofill-inline-menu-page-element.ts | 13 + ...tofill-inline-menu-content.service.spec.ts | 426 +++ .../autofill-inline-menu-content.service.ts | 437 +++ ...ll-inline-menu-iframe.service.spec.ts.snap | 11 + ...autofill-inline-menu-button-iframe.spec.ts | 27 + .../autofill-inline-menu-button-iframe.ts | 18 + ...utofill-inline-menu-iframe-element.spec.ts | 47 + .../autofill-inline-menu-iframe-element.ts | 21 + ...utofill-inline-menu-iframe.service.spec.ts | 521 +++ .../autofill-inline-menu-iframe.service.ts | 457 +++ .../autofill-inline-menu-list-iframe.spec.ts | 27 + .../autofill-inline-menu-list-iframe.ts | 23 + .../autofill-inline-menu-button.spec.ts.snap | 83 + .../autofill-inline-menu-button.spec.ts | 133 + .../button/autofill-inline-menu-button.ts | 126 + .../bootstrap-autofill-inline-menu-button.ts | 9 + .../inline-menu/pages/button/button.html | 12 + .../pages/button/button.scss | 8 +- .../autofill-inline-menu-list.spec.ts.snap | 536 +++ .../list/autofill-inline-menu-list.spec.ts | 491 +++ .../pages/list/autofill-inline-menu-list.ts | 626 ++++ .../bootstrap-autofill-inline-menu-list.ts | 9 + .../{ => inline-menu}/pages/list/list.html | 2 +- .../{ => inline-menu}/pages/list/list.scss | 18 +- .../autofill-inline-menu-container.spec.ts | 130 + .../autofill-inline-menu-container.ts | 179 + ...ootstrap-autofill-inline-menu-container.ts | 3 + .../pages/menu-container/menu-container.html | 10 + .../autofill-inline-menu-page-element.ts | 155 + .../bootstrap-autofill-overlay-button.ts | 9 - .../list/bootstrap-autofill-overlay-list.ts | 9 - .../autofill-overlay-content.service.ts | 48 +- ...nline-menu-field-qualifications.service.ts | 2 +- .../autofill/services/autofill-constants.ts | 2 +- .../autofill-overlay-content.service.spec.ts | 2076 +++++------ .../autofill-overlay-content.service.ts | 1231 ++++--- .../services/autofill.service.spec.ts | 37 +- .../src/autofill/services/autofill.service.ts | 28 +- .../collect-autofill-content.service.spec.ts | 43 +- .../collect-autofill-content.service.ts | 68 +- .../dom-element-visibility.service.ts | 7 + ...inline-menu-field-qualification.service.ts | 4 +- .../insert-autofill-content.service.spec.ts | 10 +- .../src/autofill/spec/autofill-mocks.ts | 73 +- .../src/autofill/spec/testing-utils.ts | 27 + .../autofill/utils/autofill-overlay.enum.ts | 17 - apps/browser/src/autofill/utils/index.spec.ts | 6 +- apps/browser/src/autofill/utils/index.ts | 158 +- .../browser/src/background/main.background.ts | 68 +- apps/browser/src/manifest.json | 10 +- apps/browser/src/manifest.v3.json | 10 +- .../src/popup/services/services.module.ts | 2 + apps/browser/webpack.config.js | 33 +- libs/common/src/autofill/constants/index.ts | 7 +- libs/common/src/enums/feature-flag.enum.ts | 2 + 110 files changed, 16813 insertions(+), 3940 deletions(-) create mode 100644 apps/browser/src/autofill/deprecated/background/abstractions/overlay.background.deprecated.ts create mode 100644 apps/browser/src/autofill/deprecated/background/overlay.background.deprecated.spec.ts create mode 100644 apps/browser/src/autofill/deprecated/background/overlay.background.deprecated.ts create mode 100644 apps/browser/src/autofill/deprecated/content/abstractions/autofill-init.deprecated.ts create mode 100644 apps/browser/src/autofill/deprecated/content/autofill-init.deprecated.spec.ts create mode 100644 apps/browser/src/autofill/deprecated/content/autofill-init.deprecated.ts create mode 100644 apps/browser/src/autofill/deprecated/content/bootstrap-legacy-autofill-overlay.ts rename apps/browser/src/autofill/{overlay/abstractions/autofill-overlay-button.ts => deprecated/overlay/abstractions/autofill-overlay-button.deprecated.ts} (100%) rename apps/browser/src/autofill/{overlay/abstractions/autofill-overlay-iframe.service.ts => deprecated/overlay/abstractions/autofill-overlay-iframe.service.deprecated.ts} (100%) rename apps/browser/src/autofill/{overlay/abstractions/autofill-overlay-list.ts => deprecated/overlay/abstractions/autofill-overlay-list.deprecated.ts} (96%) rename apps/browser/src/autofill/{overlay/abstractions/autofill-overlay-page-element.ts => deprecated/overlay/abstractions/autofill-overlay-page-element.deprecated.ts} (89%) rename apps/browser/src/autofill/{overlay/iframe-content/__snapshots__/autofill-overlay-iframe.service.spec.ts.snap => deprecated/overlay/iframe-content/__snapshots__/autofill-overlay-iframe.service.deprecated.spec.ts.snap} (95%) rename apps/browser/src/autofill/{overlay/iframe-content/autofill-overlay-button-iframe.spec.ts => deprecated/overlay/iframe-content/autofill-overlay-button-iframe.deprecated.spec.ts} (97%) rename apps/browser/src/autofill/{overlay/iframe-content/autofill-overlay-button-iframe.ts => deprecated/overlay/iframe-content/autofill-overlay-button-iframe.deprecated.ts} (83%) rename apps/browser/src/autofill/{overlay/iframe-content/autofill-overlay-iframe-element.spec.ts => deprecated/overlay/iframe-content/autofill-overlay-iframe-element.deprecated.spec.ts} (92%) rename apps/browser/src/autofill/{overlay/iframe-content/autofill-overlay-iframe-element.ts => deprecated/overlay/iframe-content/autofill-overlay-iframe-element.deprecated.ts} (96%) rename apps/browser/src/autofill/{overlay/iframe-content/autofill-overlay-iframe.service.spec.ts => deprecated/overlay/iframe-content/autofill-overlay-iframe.service.deprecated.spec.ts} (98%) rename apps/browser/src/autofill/{overlay/iframe-content/autofill-overlay-iframe.service.ts => deprecated/overlay/iframe-content/autofill-overlay-iframe.service.deprecated.ts} (99%) rename apps/browser/src/autofill/{overlay/iframe-content/autofill-overlay-list-iframe.spec.ts => deprecated/overlay/iframe-content/autofill-overlay-list-iframe.deprecated.spec.ts} (97%) rename apps/browser/src/autofill/{overlay/iframe-content/autofill-overlay-list-iframe.ts => deprecated/overlay/iframe-content/autofill-overlay-list-iframe.deprecated.ts} (86%) rename apps/browser/src/autofill/{overlay/pages/button/__snapshots__/autofill-overlay-button.spec.ts.snap => deprecated/overlay/pages/button/__snapshots__/autofill-overlay-button.deprecated.spec.ts.snap} (100%) rename apps/browser/src/autofill/{overlay/pages/button/autofill-overlay-button.spec.ts => deprecated/overlay/pages/button/autofill-overlay-button.deprecated.spec.ts} (78%) rename apps/browser/src/autofill/{overlay/pages/button/autofill-overlay-button.ts => deprecated/overlay/pages/button/autofill-overlay-button.deprecated.ts} (95%) create mode 100644 apps/browser/src/autofill/deprecated/overlay/pages/button/bootstrap-autofill-overlay-button.deprecated.ts rename apps/browser/src/autofill/{overlay/pages/button/button.html => deprecated/overlay/pages/button/legacy-button.html} (81%) create mode 100644 apps/browser/src/autofill/deprecated/overlay/pages/button/legacy-button.scss rename apps/browser/src/autofill/{overlay/pages/list/__snapshots__/autofill-overlay-list.spec.ts.snap => deprecated/overlay/pages/list/__snapshots__/autofill-overlay-list.deprecated.spec.ts.snap} (100%) rename apps/browser/src/autofill/{overlay/pages/list/autofill-overlay-list.spec.ts => deprecated/overlay/pages/list/autofill-overlay-list.deprecated.spec.ts} (87%) rename apps/browser/src/autofill/{overlay/pages/list/autofill-overlay-list.ts => deprecated/overlay/pages/list/autofill-overlay-list.deprecated.ts} (98%) create mode 100644 apps/browser/src/autofill/deprecated/overlay/pages/list/bootstrap-autofill-overlay-list.deprecated.ts create mode 100644 apps/browser/src/autofill/deprecated/overlay/pages/list/legacy-list.html create mode 100644 apps/browser/src/autofill/deprecated/overlay/pages/list/legacy-list.scss rename apps/browser/src/autofill/{overlay/pages/shared/autofill-overlay-page-element.spec.ts => deprecated/overlay/pages/shared/autofill-overlay-page-element.deprecated.spec.ts} (96%) rename apps/browser/src/autofill/{overlay/pages/shared/autofill-overlay-page-element.ts => deprecated/overlay/pages/shared/autofill-overlay-page-element.deprecated.ts} (96%) create mode 100644 apps/browser/src/autofill/deprecated/services/abstractions/autofill-overlay-content.service.ts create mode 100644 apps/browser/src/autofill/deprecated/services/autofill-overlay-content.service.deprecated.spec.ts create mode 100644 apps/browser/src/autofill/deprecated/services/autofill-overlay-content.service.deprecated.ts create mode 100644 apps/browser/src/autofill/enums/autofill-overlay.enum.ts rename apps/browser/src/autofill/enums/{autofill-port.enums.ts => autofill-port.enum.ts} (100%) create mode 100644 apps/browser/src/autofill/overlay/inline-menu/abstractions/autofill-inline-menu-button.ts create mode 100644 apps/browser/src/autofill/overlay/inline-menu/abstractions/autofill-inline-menu-container.ts create mode 100644 apps/browser/src/autofill/overlay/inline-menu/abstractions/autofill-inline-menu-content.service.ts create mode 100644 apps/browser/src/autofill/overlay/inline-menu/abstractions/autofill-inline-menu-iframe.service.ts create mode 100644 apps/browser/src/autofill/overlay/inline-menu/abstractions/autofill-inline-menu-list.ts create mode 100644 apps/browser/src/autofill/overlay/inline-menu/abstractions/autofill-inline-menu-page-element.ts create mode 100644 apps/browser/src/autofill/overlay/inline-menu/content/autofill-inline-menu-content.service.spec.ts create mode 100644 apps/browser/src/autofill/overlay/inline-menu/content/autofill-inline-menu-content.service.ts create mode 100644 apps/browser/src/autofill/overlay/inline-menu/iframe-content/__snapshots__/autofill-inline-menu-iframe.service.spec.ts.snap create mode 100644 apps/browser/src/autofill/overlay/inline-menu/iframe-content/autofill-inline-menu-button-iframe.spec.ts create mode 100644 apps/browser/src/autofill/overlay/inline-menu/iframe-content/autofill-inline-menu-button-iframe.ts create mode 100644 apps/browser/src/autofill/overlay/inline-menu/iframe-content/autofill-inline-menu-iframe-element.spec.ts create mode 100644 apps/browser/src/autofill/overlay/inline-menu/iframe-content/autofill-inline-menu-iframe-element.ts create mode 100644 apps/browser/src/autofill/overlay/inline-menu/iframe-content/autofill-inline-menu-iframe.service.spec.ts create mode 100644 apps/browser/src/autofill/overlay/inline-menu/iframe-content/autofill-inline-menu-iframe.service.ts create mode 100644 apps/browser/src/autofill/overlay/inline-menu/iframe-content/autofill-inline-menu-list-iframe.spec.ts create mode 100644 apps/browser/src/autofill/overlay/inline-menu/iframe-content/autofill-inline-menu-list-iframe.ts create mode 100644 apps/browser/src/autofill/overlay/inline-menu/pages/button/__snapshots__/autofill-inline-menu-button.spec.ts.snap create mode 100644 apps/browser/src/autofill/overlay/inline-menu/pages/button/autofill-inline-menu-button.spec.ts create mode 100644 apps/browser/src/autofill/overlay/inline-menu/pages/button/autofill-inline-menu-button.ts create mode 100644 apps/browser/src/autofill/overlay/inline-menu/pages/button/bootstrap-autofill-inline-menu-button.ts create mode 100644 apps/browser/src/autofill/overlay/inline-menu/pages/button/button.html rename apps/browser/src/autofill/overlay/{ => inline-menu}/pages/button/button.scss (75%) create mode 100644 apps/browser/src/autofill/overlay/inline-menu/pages/list/__snapshots__/autofill-inline-menu-list.spec.ts.snap create mode 100644 apps/browser/src/autofill/overlay/inline-menu/pages/list/autofill-inline-menu-list.spec.ts create mode 100644 apps/browser/src/autofill/overlay/inline-menu/pages/list/autofill-inline-menu-list.ts create mode 100644 apps/browser/src/autofill/overlay/inline-menu/pages/list/bootstrap-autofill-inline-menu-list.ts rename apps/browser/src/autofill/overlay/{ => inline-menu}/pages/list/list.html (81%) rename apps/browser/src/autofill/overlay/{ => inline-menu}/pages/list/list.scss (92%) create mode 100644 apps/browser/src/autofill/overlay/inline-menu/pages/menu-container/autofill-inline-menu-container.spec.ts create mode 100644 apps/browser/src/autofill/overlay/inline-menu/pages/menu-container/autofill-inline-menu-container.ts create mode 100644 apps/browser/src/autofill/overlay/inline-menu/pages/menu-container/bootstrap-autofill-inline-menu-container.ts create mode 100644 apps/browser/src/autofill/overlay/inline-menu/pages/menu-container/menu-container.html create mode 100644 apps/browser/src/autofill/overlay/inline-menu/pages/shared/autofill-inline-menu-page-element.ts delete mode 100644 apps/browser/src/autofill/overlay/pages/button/bootstrap-autofill-overlay-button.ts delete mode 100644 apps/browser/src/autofill/overlay/pages/list/bootstrap-autofill-overlay-list.ts delete mode 100644 apps/browser/src/autofill/utils/autofill-overlay.enum.ts diff --git a/apps/browser/src/autofill/background/abstractions/overlay.background.ts b/apps/browser/src/autofill/background/abstractions/overlay.background.ts index aa62194af5c..462acb818b8 100644 --- a/apps/browser/src/autofill/background/abstractions/overlay.background.ts +++ b/apps/browser/src/autofill/background/abstractions/overlay.background.ts @@ -2,17 +2,43 @@ import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type"; import AutofillPageDetails from "../../models/autofill-page-details"; +import { PageDetail } from "../../services/abstractions/autofill.service"; import { LockedVaultPendingNotificationsData } from "./notification.background"; -type WebsiteIconData = { +export type PageDetailsForTab = Record< + chrome.runtime.MessageSender["tab"]["id"], + Map +>; + +export type SubFrameOffsetData = { + top: number; + left: number; + url?: string; + frameId?: number; + parentFrameIds?: number[]; +} | null; + +export type SubFrameOffsetsForTab = Record< + chrome.runtime.MessageSender["tab"]["id"], + Map +>; + +export type WebsiteIconData = { imageEnabled: boolean; image: string; fallbackImage: string; icon: string; }; -type OverlayAddNewItemMessage = { +export type FocusedFieldData = { + focusedFieldStyles: Partial; + focusedFieldRects: Partial; + tabId?: number; + frameId?: number; +}; + +export type OverlayAddNewItemMessage = { login?: { uri?: string; hostname: string; @@ -21,112 +47,132 @@ type OverlayAddNewItemMessage = { }; }; -type OverlayBackgroundExtensionMessage = { - [key: string]: any; +export type CloseInlineMenuMessage = { + forceCloseInlineMenu?: boolean; + overlayElement?: string; +}; + +export type ToggleInlineMenuHiddenMessage = { + isInlineMenuHidden?: boolean; + setTransparentInlineMenu?: boolean; +}; + +export type OverlayBackgroundExtensionMessage = { command: string; + portKey?: string; tab?: chrome.tabs.Tab; sender?: string; details?: AutofillPageDetails; - overlayElement?: string; - display?: string; + isFieldCurrentlyFocused?: boolean; + isFieldCurrentlyFilling?: boolean; + isVisible?: boolean; + subFrameData?: SubFrameOffsetData; + focusedFieldData?: FocusedFieldData; + styles?: Partial; data?: LockedVaultPendingNotificationsData; -} & OverlayAddNewItemMessage; +} & OverlayAddNewItemMessage & + CloseInlineMenuMessage & + ToggleInlineMenuHiddenMessage; -type OverlayPortMessage = { +export type OverlayPortMessage = { [key: string]: any; command: string; direction?: string; - overlayCipherId?: string; + inlineMenuCipherId?: string; }; -type FocusedFieldData = { - focusedFieldStyles: Partial; - focusedFieldRects: Partial; - tabId?: number; -}; - -type OverlayCipherData = { +export type InlineMenuCipherData = { id: string; name: string; type: CipherType; reprompt: CipherRepromptType; favorite: boolean; - icon: { imageEnabled: boolean; image: string; fallbackImage: string; icon: string }; + icon: WebsiteIconData; login?: { username: string }; card?: string; }; -type BackgroundMessageParam = { +export type BackgroundMessageParam = { message: OverlayBackgroundExtensionMessage; }; -type BackgroundSenderParam = { +export type BackgroundSenderParam = { sender: chrome.runtime.MessageSender; }; -type BackgroundOnMessageHandlerParams = BackgroundMessageParam & BackgroundSenderParam; +export type BackgroundOnMessageHandlerParams = BackgroundMessageParam & BackgroundSenderParam; -type OverlayBackgroundExtensionMessageHandlers = { +export type OverlayBackgroundExtensionMessageHandlers = { [key: string]: CallableFunction; - openAutofillOverlay: () => void; autofillOverlayElementClosed: ({ message, sender }: BackgroundOnMessageHandlerParams) => void; autofillOverlayAddNewVaultItem: ({ message, sender }: BackgroundOnMessageHandlerParams) => void; - getAutofillOverlayVisibility: () => void; - checkAutofillOverlayFocused: () => void; - focusAutofillOverlayList: () => void; - updateAutofillOverlayPosition: ({ message, sender }: BackgroundOnMessageHandlerParams) => void; - updateAutofillOverlayHidden: ({ message }: BackgroundMessageParam) => void; + triggerAutofillOverlayReposition: ({ message, sender }: BackgroundOnMessageHandlerParams) => void; + checkIsInlineMenuCiphersPopulated: ({ sender }: BackgroundSenderParam) => void; updateFocusedFieldData: ({ message, sender }: BackgroundOnMessageHandlerParams) => void; + updateIsFieldCurrentlyFocused: ({ message }: BackgroundMessageParam) => void; + checkIsFieldCurrentlyFocused: () => boolean; + updateIsFieldCurrentlyFilling: ({ message }: BackgroundMessageParam) => void; + checkIsFieldCurrentlyFilling: () => boolean; + getAutofillInlineMenuVisibility: () => void; + openAutofillInlineMenu: () => void; + closeAutofillInlineMenu: ({ message, sender }: BackgroundOnMessageHandlerParams) => void; + checkAutofillInlineMenuFocused: ({ sender }: BackgroundSenderParam) => void; + focusAutofillInlineMenuList: () => void; + updateAutofillInlineMenuPosition: ({ + message, + sender, + }: BackgroundOnMessageHandlerParams) => Promise; + updateAutofillInlineMenuElementIsVisibleStatus: ({ + message, + sender, + }: BackgroundOnMessageHandlerParams) => void; + checkIsAutofillInlineMenuButtonVisible: () => void; + checkIsAutofillInlineMenuListVisible: () => void; + getCurrentTabFrameId: ({ sender }: BackgroundSenderParam) => number; + updateSubFrameData: ({ message, sender }: BackgroundOnMessageHandlerParams) => void; + triggerSubFrameFocusInRebuild: ({ sender }: BackgroundSenderParam) => void; + destroyAutofillInlineMenuListeners: ({ + message, + sender, + }: BackgroundOnMessageHandlerParams) => void; collectPageDetailsResponse: ({ message, sender }: BackgroundOnMessageHandlerParams) => void; unlockCompleted: ({ message }: BackgroundMessageParam) => void; + doFullSync: () => void; addedCipher: () => void; addEditCipherSubmitted: () => void; editedCipher: () => void; deletedCipher: () => void; }; -type PortMessageParam = { +export type PortMessageParam = { message: OverlayPortMessage; }; -type PortConnectionParam = { +export type PortConnectionParam = { port: chrome.runtime.Port; }; -type PortOnMessageHandlerParams = PortMessageParam & PortConnectionParam; +export type PortOnMessageHandlerParams = PortMessageParam & PortConnectionParam; -type OverlayButtonPortMessageHandlers = { +export type InlineMenuButtonPortMessageHandlers = { [key: string]: CallableFunction; - overlayButtonClicked: ({ port }: PortConnectionParam) => void; - closeAutofillOverlay: ({ port }: PortConnectionParam) => void; - forceCloseAutofillOverlay: ({ port }: PortConnectionParam) => void; - overlayPageBlurred: () => void; - redirectOverlayFocusOut: ({ message, port }: PortOnMessageHandlerParams) => void; + triggerDelayedAutofillInlineMenuClosure: ({ port }: PortConnectionParam) => void; + autofillInlineMenuButtonClicked: ({ port }: PortConnectionParam) => void; + autofillInlineMenuBlurred: () => void; + redirectAutofillInlineMenuFocusOut: ({ message, port }: PortOnMessageHandlerParams) => void; + updateAutofillInlineMenuColorScheme: () => void; }; -type OverlayListPortMessageHandlers = { +export type InlineMenuListPortMessageHandlers = { [key: string]: CallableFunction; - checkAutofillOverlayButtonFocused: () => void; - forceCloseAutofillOverlay: ({ port }: PortConnectionParam) => void; - overlayPageBlurred: () => void; + checkAutofillInlineMenuButtonFocused: () => void; + autofillInlineMenuBlurred: () => void; unlockVault: ({ port }: PortConnectionParam) => void; - fillSelectedListItem: ({ message, port }: PortOnMessageHandlerParams) => void; + fillAutofillInlineMenuCipher: ({ message, port }: PortOnMessageHandlerParams) => void; addNewVaultItem: ({ port }: PortConnectionParam) => void; viewSelectedCipher: ({ message, port }: PortOnMessageHandlerParams) => void; - redirectOverlayFocusOut: ({ message, port }: PortOnMessageHandlerParams) => void; + redirectAutofillInlineMenuFocusOut: ({ message, port }: PortOnMessageHandlerParams) => void; + updateAutofillInlineMenuListHeight: ({ message, port }: PortOnMessageHandlerParams) => void; }; -interface OverlayBackground { +export interface OverlayBackground { init(): Promise; removePageDetails(tabId: number): void; - updateOverlayCiphers(): void; + updateOverlayCiphers(): Promise; } - -export { - WebsiteIconData, - OverlayBackgroundExtensionMessage, - OverlayPortMessage, - FocusedFieldData, - OverlayCipherData, - OverlayAddNewItemMessage, - OverlayBackgroundExtensionMessageHandlers, - OverlayButtonPortMessageHandlers, - OverlayListPortMessageHandlers, - OverlayBackground, -}; diff --git a/apps/browser/src/autofill/background/notification.background.ts b/apps/browser/src/autofill/background/notification.background.ts index 179598a8823..9e989b73e62 100644 --- a/apps/browser/src/autofill/background/notification.background.ts +++ b/apps/browser/src/autofill/background/notification.background.ts @@ -770,12 +770,12 @@ export default class NotificationBackground { ) => { const handler: CallableFunction | undefined = this.extensionMessageHandlers[message?.command]; if (!handler) { - return; + return null; } const messageResponse = handler({ message, sender }); - if (!messageResponse) { - return; + if (typeof messageResponse === "undefined") { + return null; } Promise.resolve(messageResponse) diff --git a/apps/browser/src/autofill/background/overlay.background.spec.ts b/apps/browser/src/autofill/background/overlay.background.spec.ts index 7be93b11e6b..81a7754f84b 100644 --- a/apps/browser/src/autofill/background/overlay.background.spec.ts +++ b/apps/browser/src/autofill/background/overlay.background.spec.ts @@ -1,102 +1,161 @@ import { mock, MockProxy, mockReset } from "jest-mock-extended"; -import { BehaviorSubject, of } from "rxjs"; +import { BehaviorSubject } from "rxjs"; +import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; -import { AuthService } from "@bitwarden/common/auth/services/auth.service"; import { - SHOW_AUTOFILL_BUTTON, AutofillOverlayVisibility, + SHOW_AUTOFILL_BUTTON, } from "@bitwarden/common/autofill/constants"; -import { AutofillSettingsService } from "@bitwarden/common/autofill/services/autofill-settings.service"; +import { AutofillSettingsServiceAbstraction as AutofillSettingsService } from "@bitwarden/common/autofill/services/autofill-settings.service"; import { DefaultDomainSettingsService, DomainSettingsService, } from "@bitwarden/common/autofill/services/domain-settings.service"; +import { InlineMenuVisibilitySetting } from "@bitwarden/common/autofill/types"; import { EnvironmentService, Region, } 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"; import { ThemeType } from "@bitwarden/common/platform/enums"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { CloudEnvironment } from "@bitwarden/common/platform/services/default-environment.service"; -import { I18nService } from "@bitwarden/common/platform/services/i18n.service"; import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; import { - FakeStateProvider, FakeAccountService, + FakeStateProvider, mockAccountServiceWith, } from "@bitwarden/common/spec"; import { UserId } from "@bitwarden/common/types/guid"; -import { CipherType } from "@bitwarden/common/vault/enums"; -import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { CipherRepromptType, CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; -import { CipherService } from "@bitwarden/common/vault/services/cipher.service"; import { BrowserApi } from "../../platform/browser/browser-api"; import { BrowserPlatformUtilsService } from "../../platform/services/platform-utils/browser-platform-utils.service"; -import { AutofillService } from "../services/abstractions/autofill.service"; -import { - createAutofillPageDetailsMock, - createChromeTabMock, - createFocusedFieldDataMock, - createPageDetailMock, - createPortSpyMock, -} from "../spec/autofill-mocks"; -import { flushPromises, sendMockExtensionMessage, sendPortMessage } from "../spec/testing-utils"; import { AutofillOverlayElement, AutofillOverlayPort, + MAX_SUB_FRAME_DEPTH, RedirectFocusDirection, -} from "../utils/autofill-overlay.enum"; +} from "../enums/autofill-overlay.enum"; +import { AutofillService } from "../services/abstractions/autofill.service"; +import { + createChromeTabMock, + createAutofillPageDetailsMock, + createPortSpyMock, + createFocusedFieldDataMock, + createPageDetailMock, +} from "../spec/autofill-mocks"; +import { + flushPromises, + sendMockExtensionMessage, + sendPortMessage, + triggerPortOnConnectEvent, + triggerPortOnDisconnectEvent, + triggerPortOnMessageEvent, + triggerWebNavigationOnCommittedEvent, +} from "../spec/testing-utils"; -import OverlayBackground from "./overlay.background"; +import { + FocusedFieldData, + PageDetailsForTab, + SubFrameOffsetData, + SubFrameOffsetsForTab, +} from "./abstractions/overlay.background"; +import { OverlayBackground } from "./overlay.background"; describe("OverlayBackground", () => { const mockUserId = Utils.newGuid() as UserId; - const accountService: FakeAccountService = mockAccountServiceWith(mockUserId); - const fakeStateProvider: FakeStateProvider = new FakeStateProvider(accountService); + const sendResponse = jest.fn(); + let accountService: FakeAccountService; + let fakeStateProvider: FakeStateProvider; + let showFaviconsMock$: BehaviorSubject; let domainSettingsService: DomainSettingsService; - let buttonPortSpy: chrome.runtime.Port; - let listPortSpy: chrome.runtime.Port; - let overlayBackground: OverlayBackground; - const cipherService = mock(); - const autofillService = mock(); + let logService: MockProxy; + let cipherService: MockProxy; + let autofillService: MockProxy; let activeAccountStatusMock$: BehaviorSubject; let authService: MockProxy; + let environmentMock$: BehaviorSubject; + let environmentService: MockProxy; + let inlineMenuVisibilityMock$: BehaviorSubject; + let autofillSettingsService: MockProxy; + let i18nService: MockProxy; + let platformUtilsService: MockProxy; + let selectedThemeMock$: BehaviorSubject; + let themeStateService: MockProxy; + let overlayBackground: OverlayBackground; + let portKeyForTabSpy: Record; + let pageDetailsForTabSpy: PageDetailsForTab; + let subFrameOffsetsSpy: SubFrameOffsetsForTab; + let getFrameDetailsSpy: jest.SpyInstance; + let tabsSendMessageSpy: jest.SpyInstance; + let tabSendMessageDataSpy: jest.SpyInstance; + let sendMessageSpy: jest.SpyInstance; + let getTabFromCurrentWindowIdSpy: jest.SpyInstance; + let getTabSpy: jest.SpyInstance; + let openUnlockPopoutSpy: jest.SpyInstance; + let buttonPortSpy: chrome.runtime.Port; + let buttonMessageConnectorSpy: chrome.runtime.Port; + let listPortSpy: chrome.runtime.Port; + let listMessageConnectorSpy: chrome.runtime.Port; - const environmentService = mock(); - environmentService.environment$ = new BehaviorSubject( - new CloudEnvironment({ - key: Region.US, - domain: "bitwarden.com", - urls: { icons: "https://icons.bitwarden.com/" }, - }), - ); - const autofillSettingsService = mock(); - const i18nService = mock(); - const platformUtilsService = mock(); - const themeStateService = mock(); - const initOverlayElementPorts = async (options = { initList: true, initButton: true }) => { + let getFrameCounter: number = 2; + async function initOverlayElementPorts(options = { initList: true, initButton: true }) { const { initList, initButton } = options; if (initButton) { - await overlayBackground["handlePortOnConnect"](createPortSpyMock(AutofillOverlayPort.Button)); - buttonPortSpy = overlayBackground["overlayButtonPort"]; + triggerPortOnConnectEvent(createPortSpyMock(AutofillOverlayPort.Button)); + buttonPortSpy = overlayBackground["inlineMenuButtonPort"]; + + buttonMessageConnectorSpy = createPortSpyMock(AutofillOverlayPort.ButtonMessageConnector); + triggerPortOnConnectEvent(buttonMessageConnectorSpy); } if (initList) { - await overlayBackground["handlePortOnConnect"](createPortSpyMock(AutofillOverlayPort.List)); - listPortSpy = overlayBackground["overlayListPort"]; + triggerPortOnConnectEvent(createPortSpyMock(AutofillOverlayPort.List)); + listPortSpy = overlayBackground["inlineMenuListPort"]; + + listMessageConnectorSpy = createPortSpyMock(AutofillOverlayPort.ListMessageConnector); + triggerPortOnConnectEvent(listMessageConnectorSpy); } return { buttonPortSpy, listPortSpy }; - }; + } beforeEach(() => { + accountService = mockAccountServiceWith(mockUserId); + fakeStateProvider = new FakeStateProvider(accountService); + showFaviconsMock$ = new BehaviorSubject(true); domainSettingsService = new DefaultDomainSettingsService(fakeStateProvider); + domainSettingsService.showFavicons$ = showFaviconsMock$; + logService = mock(); + cipherService = mock(); + autofillService = mock(); activeAccountStatusMock$ = new BehaviorSubject(AuthenticationStatus.Unlocked); authService = mock(); authService.activeAccountStatus$ = activeAccountStatusMock$; + environmentMock$ = new BehaviorSubject( + new CloudEnvironment({ + key: Region.US, + domain: "bitwarden.com", + urls: { icons: "https://icons.bitwarden.com/" }, + }), + ); + environmentService = mock(); + environmentService.environment$ = environmentMock$; + inlineMenuVisibilityMock$ = new BehaviorSubject(AutofillOverlayVisibility.OnFieldFocus); + autofillSettingsService = mock(); + autofillSettingsService.inlineMenuVisibility$ = inlineMenuVisibilityMock$; + i18nService = mock(); + platformUtilsService = mock(); + selectedThemeMock$ = new BehaviorSubject(ThemeType.Light); + themeStateService = mock(); + themeStateService.selectedTheme$ = selectedThemeMock$; overlayBackground = new OverlayBackground( + logService, cipherService, autofillService, authService, @@ -107,48 +166,528 @@ describe("OverlayBackground", () => { platformUtilsService, themeStateService, ); - - jest - .spyOn(overlayBackground as any, "getOverlayVisibility") - .mockResolvedValue(AutofillOverlayVisibility.OnFieldFocus); - - themeStateService.selectedTheme$ = of(ThemeType.Light); - domainSettingsService.showFavicons$ = of(true); + portKeyForTabSpy = overlayBackground["portKeyForTab"]; + pageDetailsForTabSpy = overlayBackground["pageDetailsForTab"]; + subFrameOffsetsSpy = overlayBackground["subFrameOffsetsForTab"]; + getFrameDetailsSpy = jest.spyOn(BrowserApi, "getFrameDetails"); + getFrameDetailsSpy.mockImplementation((_details: chrome.webNavigation.GetFrameDetails) => { + getFrameCounter--; + return mock({ + parentFrameId: getFrameCounter, + }); + }); + tabsSendMessageSpy = jest.spyOn(BrowserApi, "tabSendMessage"); + tabSendMessageDataSpy = jest.spyOn(BrowserApi, "tabSendMessageData"); + sendMessageSpy = jest.spyOn(BrowserApi, "sendMessage"); + getTabFromCurrentWindowIdSpy = jest.spyOn(BrowserApi, "getTabFromCurrentWindowId"); + getTabSpy = jest.spyOn(BrowserApi, "getTab"); + openUnlockPopoutSpy = jest.spyOn(overlayBackground as any, "openUnlockPopout"); void overlayBackground.init(); }); afterEach(() => { + getFrameCounter = 2; jest.clearAllMocks(); + jest.useRealTimers(); mockReset(cipherService); }); - describe("removePageDetails", () => { - it("removes the page details for a specific tab from the pageDetailsForTab object", () => { + describe("storing pageDetails", () => { + const tabId = 1; + + beforeEach(() => { + sendMockExtensionMessage( + { command: "collectPageDetailsResponse", details: createAutofillPageDetailsMock() }, + mock({ tab: createChromeTabMock({ id: tabId }), frameId: 0 }), + ); + }); + + it("stores the page details for the tab", () => { + expect(pageDetailsForTabSpy[tabId]).toBeDefined(); + }); + + describe("building sub frame offsets", () => { + beforeEach(() => { + tabsSendMessageSpy.mockResolvedValue( + mock({ + left: getFrameCounter, + top: getFrameCounter, + url: "url", + }), + ); + }); + + it("triggers a destruction of the inline menu listeners if the max frame depth is exceeded ", async () => { + getFrameCounter = MAX_SUB_FRAME_DEPTH + 1; + const tab = createChromeTabMock({ id: tabId }); + sendMockExtensionMessage( + { command: "collectPageDetailsResponse", details: createAutofillPageDetailsMock() }, + mock({ + tab, + frameId: 1, + }), + ); + await flushPromises(); + + expect(tabsSendMessageSpy).toHaveBeenCalledWith( + tab, + { command: "destroyAutofillInlineMenuListeners" }, + { frameId: 1 }, + ); + }); + + it("builds the offset values for a sub frame within the tab", async () => { + sendMockExtensionMessage( + { command: "collectPageDetailsResponse", details: createAutofillPageDetailsMock() }, + mock({ + tab: createChromeTabMock({ id: tabId }), + frameId: 1, + }), + ); + await flushPromises(); + + expect(subFrameOffsetsSpy[tabId]).toStrictEqual( + new Map([[1, { left: 4, top: 4, url: "url", parentFrameIds: [0, 1] }]]), + ); + expect(pageDetailsForTabSpy[tabId].size).toBe(2); + }); + + it("skips building offset values for a previously calculated sub frame", async () => { + getFrameCounter = 0; + sendMockExtensionMessage( + { command: "collectPageDetailsResponse", details: createAutofillPageDetailsMock() }, + mock({ + tab: createChromeTabMock({ id: tabId }), + frameId: 1, + }), + ); + await flushPromises(); + + sendMockExtensionMessage( + { command: "collectPageDetailsResponse", details: createAutofillPageDetailsMock() }, + mock({ + tab: createChromeTabMock({ id: tabId }), + frameId: 1, + }), + ); + await flushPromises(); + + expect(getFrameDetailsSpy).toHaveBeenCalledTimes(1); + expect(subFrameOffsetsSpy[tabId]).toStrictEqual( + new Map([[1, { left: 0, top: 0, url: "url", parentFrameIds: [0] }]]), + ); + }); + + it("will attempt to build the sub frame offsets by posting window messages if a set of offsets is not returned", async () => { + const tab = createChromeTabMock({ id: tabId }); + const frameId = 1; + tabsSendMessageSpy.mockResolvedValue(null); + sendMockExtensionMessage( + { command: "collectPageDetailsResponse", details: createAutofillPageDetailsMock() }, + mock({ tab, frameId }), + ); + await flushPromises(); + + expect(tabsSendMessageSpy).toHaveBeenCalledWith( + tab, + { + command: "getSubFrameOffsetsFromWindowMessage", + subFrameId: frameId, + }, + { frameId }, + ); + expect(subFrameOffsetsSpy[tabId]).toStrictEqual(new Map([[frameId, null]])); + }); + + it("updates sub frame data that has been calculated using window messages", async () => { + const tab = createChromeTabMock({ id: tabId }); + const frameId = 1; + const subFrameData = mock({ frameId, left: 10, top: 10, url: "url" }); + tabsSendMessageSpy.mockResolvedValueOnce(null); + subFrameOffsetsSpy[tabId] = new Map([[frameId, null]]); + + sendMockExtensionMessage( + { command: "updateSubFrameData", subFrameData }, + mock({ tab, frameId }), + ); + await flushPromises(); + + expect(subFrameOffsetsSpy[tabId]).toStrictEqual(new Map([[frameId, subFrameData]])); + }); + }); + }); + + describe("removing pageDetails", () => { + it("removes the page details and port key for a specific tab from the pageDetailsForTab object", () => { const tabId = 1; - const frameId = 2; - overlayBackground["pageDetailsForTab"][tabId] = new Map([[frameId, createPageDetailMock()]]); + sendMockExtensionMessage( + { command: "collectPageDetailsResponse", details: createAutofillPageDetailsMock() }, + mock({ tab: createChromeTabMock({ id: tabId }), frameId: 1 }), + ); + overlayBackground.removePageDetails(tabId); - expect(overlayBackground["pageDetailsForTab"][tabId]).toBeUndefined(); + expect(pageDetailsForTabSpy[tabId]).toBeUndefined(); + expect(portKeyForTabSpy[tabId]).toBeUndefined(); }); }); - describe("init", () => { - it("sets up the extension message listeners, get the overlay's visibility settings, and get the user's auth status", async () => { - overlayBackground["setupExtensionMessageListeners"] = jest.fn(); - overlayBackground["getOverlayVisibility"] = jest.fn(); - overlayBackground["getAuthStatus"] = jest.fn(); + describe("re-positioning the inline menu within sub frames", () => { + const tabId = 1; + const topFrameId = 0; + const middleFrameId = 10; + const middleAdjacentFrameId = 11; + const bottomFrameId = 20; + let tab: chrome.tabs.Tab; + let sender: MockProxy; - await overlayBackground.init(); + async function flushOverlayRepositionPromises() { + await flushPromises(); + jest.advanceTimersByTime(1150); + await flushPromises(); + } - expect(overlayBackground["setupExtensionMessageListeners"]).toHaveBeenCalled(); - expect(overlayBackground["getOverlayVisibility"]).toHaveBeenCalled(); - expect(overlayBackground["getAuthStatus"]).toHaveBeenCalled(); + beforeEach(() => { + jest.useFakeTimers(); + tab = createChromeTabMock({ id: tabId }); + sender = mock({ tab, frameId: middleFrameId }); + overlayBackground["focusedFieldData"] = mock({ + tabId, + frameId: bottomFrameId, + }); + subFrameOffsetsSpy[tabId] = new Map([ + [topFrameId, { left: 1, top: 1, url: "https://top-frame.com", parentFrameIds: [] }], + [ + middleFrameId, + { left: 2, top: 2, url: "https://middle-frame.com", parentFrameIds: [topFrameId] }, + ], + [ + middleAdjacentFrameId, + { + left: 3, + top: 3, + url: "https://middle-adjacent-frame.com", + parentFrameIds: [topFrameId], + }, + ], + [ + bottomFrameId, + { + left: 4, + top: 4, + url: "https://bottom-frame.com", + parentFrameIds: [topFrameId, middleFrameId], + }, + ], + ]); + tabsSendMessageSpy.mockResolvedValue( + mock({ + left: getFrameCounter, + top: getFrameCounter, + url: "url", + }), + ); + }); + + describe("triggerAutofillOverlayReposition", () => { + describe("checkShouldRepositionInlineMenu", () => { + let focusedFieldData: FocusedFieldData; + let repositionInlineMenuSpy: jest.SpyInstance; + + beforeEach(() => { + focusedFieldData = createFocusedFieldDataMock({ tabId }); + repositionInlineMenuSpy = jest.spyOn(overlayBackground as any, "repositionInlineMenu"); + }); + + describe("blocking a reposition of the overlay", () => { + it("blocks repositioning when the focused field data is not set", async () => { + overlayBackground["focusedFieldData"] = undefined; + + sendMockExtensionMessage({ command: "triggerAutofillOverlayReposition" }, sender); + await flushOverlayRepositionPromises(); + + expect(repositionInlineMenuSpy).not.toHaveBeenCalled(); + }); + + it("blocks repositioning when the sender is from a different tab than the focused field", async () => { + const otherSender = mock({ frameId: 1, tab: { id: 2 } }); + sendMockExtensionMessage( + { command: "updateFocusedFieldData", focusedFieldData }, + otherSender, + ); + + sendMockExtensionMessage({ command: "triggerAutofillOverlayReposition" }, sender); + await flushOverlayRepositionPromises(); + + expect(repositionInlineMenuSpy).not.toHaveBeenCalled(); + }); + + it("blocks repositioning when the sender frame is not a parent frame of the focused field", async () => { + focusedFieldData = createFocusedFieldDataMock({ tabId }); + const otherFrameSender = mock({ + tab, + frameId: middleAdjacentFrameId, + }); + sendMockExtensionMessage( + { command: "updateFocusedFieldData", focusedFieldData }, + otherFrameSender, + ); + sender.frameId = bottomFrameId; + + sendMockExtensionMessage({ command: "triggerAutofillOverlayReposition" }, sender); + await flushOverlayRepositionPromises(); + + expect(repositionInlineMenuSpy).not.toHaveBeenCalled(); + }); + }); + + describe("allowing a reposition of the overlay", () => { + it("allows repositioning when the sender frame is for the focused field and the inline menu is visible, ", async () => { + sendMockExtensionMessage( + { command: "updateFocusedFieldData", focusedFieldData }, + sender, + ); + tabsSendMessageSpy.mockImplementation((_tab, message) => { + if (message.command === "checkIsAutofillInlineMenuButtonVisible") { + return Promise.resolve(true); + } + }); + + sendMockExtensionMessage({ command: "triggerAutofillOverlayReposition" }, sender); + await flushOverlayRepositionPromises(); + + expect(repositionInlineMenuSpy).toHaveBeenCalled(); + }); + }); + }); + + describe("repositionInlineMenu", () => { + beforeEach(() => { + overlayBackground["isFieldCurrentlyFocused"] = true; + }); + + it("closes the inline menu if the field is not focused", async () => { + overlayBackground["isFieldCurrentlyFocused"] = false; + + sendMockExtensionMessage({ command: "triggerAutofillOverlayReposition" }, sender); + await flushOverlayRepositionPromises(); + + expect(tabsSendMessageSpy).toHaveBeenCalledWith( + tab, + { command: "closeAutofillInlineMenu" }, + { frameId: 0 }, + ); + }); + + it("closes the inline menu if the focused field is not within the viewport", async () => { + tabsSendMessageSpy.mockImplementation((_tab, message) => { + if (message.command === "checkIsMostRecentlyFocusedFieldWithinViewport") { + return Promise.resolve(false); + } + }); + + sendMockExtensionMessage({ command: "triggerAutofillOverlayReposition" }, sender); + await flushOverlayRepositionPromises(); + + expect(tabsSendMessageSpy).toHaveBeenCalledWith( + tab, + { command: "closeAutofillInlineMenu" }, + { frameId: 0 }, + ); + }); + + it("rebuilds the sub frame offsets when the focused field's frame id indicates that it is within a sub frame", async () => { + const focusedFieldData = createFocusedFieldDataMock({ tabId, frameId: middleFrameId }); + sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }, sender); + + sendMockExtensionMessage({ command: "triggerAutofillOverlayReposition" }, sender); + await flushOverlayRepositionPromises(); + + expect(getFrameDetailsSpy).toHaveBeenCalledWith({ tabId, frameId: middleFrameId }); + }); + + describe("updating the inline menu position", () => { + let sender: chrome.runtime.MessageSender; + + async function flushUpdateInlineMenuPromises() { + await flushOverlayRepositionPromises(); + await flushPromises(); + jest.advanceTimersByTime(250); + await flushPromises(); + } + + beforeEach(async () => { + sender = mock({ tab, frameId: middleFrameId }); + jest.useFakeTimers(); + await initOverlayElementPorts(); + }); + + it("skips updating the position of either inline menu element if a field is not currently focused", async () => { + sendMockExtensionMessage({ + command: "updateIsFieldCurrentlyFocused", + isFieldCurrentlyFocused: false, + }); + + sendMockExtensionMessage({ command: "triggerAutofillOverlayReposition" }, sender); + await flushUpdateInlineMenuPromises(); + + expect(tabsSendMessageSpy).not.toHaveBeenCalledWith( + sender.tab, + { + command: "appendAutofillInlineMenuToDom", + overlayElement: AutofillOverlayElement.Button, + }, + { frameId: 0 }, + ); + expect(tabsSendMessageSpy).not.toHaveBeenCalledWith( + sender.tab, + { + command: "appendAutofillInlineMenuToDom", + overlayElement: AutofillOverlayElement.List, + }, + { frameId: 0 }, + ); + }); + + it("sets the inline menu invisible and updates its position", async () => { + overlayBackground["checkIsInlineMenuButtonVisible"] = jest + .fn() + .mockResolvedValue(false); + + sendMockExtensionMessage({ command: "triggerAutofillOverlayReposition" }, sender); + await flushUpdateInlineMenuPromises(); + + expect(buttonPortSpy.postMessage).toHaveBeenCalledWith({ + command: "toggleAutofillInlineMenuHidden", + styles: { display: "none" }, + }); + expect(tabsSendMessageSpy).toHaveBeenCalledWith( + sender.tab, + { + command: "appendAutofillInlineMenuToDom", + overlayElement: AutofillOverlayElement.Button, + }, + { frameId: 0 }, + ); + expect(tabsSendMessageSpy).toHaveBeenCalledWith( + sender.tab, + { + command: "appendAutofillInlineMenuToDom", + overlayElement: AutofillOverlayElement.List, + }, + { frameId: 0 }, + ); + }); + + it("skips updating the inline menu list if the user has the inline menu set to open on button click", async () => { + inlineMenuVisibilityMock$.next(AutofillOverlayVisibility.OnButtonClick); + tabsSendMessageSpy.mockImplementation((_tab, message, _options) => { + if (message.command === "checkMostRecentlyFocusedFieldHasValue") { + return Promise.resolve(true); + } + + return Promise.resolve({}); + }); + + sendMockExtensionMessage({ command: "triggerAutofillOverlayReposition" }, sender); + await flushUpdateInlineMenuPromises(); + + expect(tabsSendMessageSpy).toHaveBeenCalledWith( + sender.tab, + { + command: "appendAutofillInlineMenuToDom", + overlayElement: AutofillOverlayElement.Button, + }, + { frameId: 0 }, + ); + expect(tabsSendMessageSpy).not.toHaveBeenCalledWith( + sender.tab, + { + command: "appendAutofillInlineMenuToDom", + overlayElement: AutofillOverlayElement.List, + }, + { frameId: 0 }, + ); + }); + + it("skips updating the inline menu list if the focused field has a value and the user status is not unlocked", async () => { + activeAccountStatusMock$.next(AuthenticationStatus.Locked); + tabsSendMessageSpy.mockImplementation((_tab, message, _options) => { + if (message.command === "checkMostRecentlyFocusedFieldHasValue") { + return Promise.resolve(true); + } + + return Promise.resolve({}); + }); + + sendMockExtensionMessage({ command: "triggerAutofillOverlayReposition" }, sender); + await flushUpdateInlineMenuPromises(); + + expect(tabsSendMessageSpy).toHaveBeenCalledWith( + sender.tab, + { + command: "appendAutofillInlineMenuToDom", + overlayElement: AutofillOverlayElement.Button, + }, + { frameId: 0 }, + ); + expect(tabsSendMessageSpy).not.toHaveBeenCalledWith( + sender.tab, + { + command: "appendAutofillInlineMenuToDom", + overlayElement: AutofillOverlayElement.List, + }, + { frameId: 0 }, + ); + }); + }); + }); + + describe("triggerSubFrameFocusInRebuild", () => { + it("triggers a rebuild of the sub frame and updates the inline menu position", async () => { + const rebuildSubFrameOffsetsSpy = jest.spyOn( + overlayBackground as any, + "rebuildSubFrameOffsets", + ); + const repositionInlineMenuSpy = jest.spyOn( + overlayBackground as any, + "repositionInlineMenu", + ); + + sendMockExtensionMessage({ command: "triggerSubFrameFocusInRebuild" }, sender); + await flushOverlayRepositionPromises(); + + expect(rebuildSubFrameOffsetsSpy).toHaveBeenCalled(); + expect(repositionInlineMenuSpy).toHaveBeenCalled(); + }); + }); + + describe("toggleInlineMenuHidden", () => { + beforeEach(async () => { + await initOverlayElementPorts(); + }); + + it("skips adjusting the hidden status of the inline menu if the sender tab does not contain the focused field", async () => { + const focusedFieldData = createFocusedFieldDataMock({ tabId: 2 }); + sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }, sender); + const otherSender = mock({ tab: { id: 2 } }); + + await overlayBackground["toggleInlineMenuHidden"]( + { isInlineMenuHidden: true }, + otherSender, + ); + + expect(buttonPortSpy.postMessage).not.toHaveBeenCalledWith({ + command: "toggleAutofillInlineMenuHidden", + styles: { display: "none" }, + }); + }); + }); }); }); - describe("updateOverlayCiphers", () => { + describe("updating the overlay ciphers", () => { const url = "https://jest-testing-website.com"; const tab = createChromeTabMock({ url }); const cipher1 = mock({ @@ -160,86 +699,100 @@ describe("OverlayBackground", () => { }); const cipher2 = mock({ id: "id-2", - localData: { lastUsedDate: 111 }, + localData: { lastUsedDate: 222 }, name: "name-2", - type: CipherType.Login, - login: { username: "username-2", uri: url }, + type: CipherType.Card, + card: { subTitle: "subtitle-2" }, }); beforeEach(() => { activeAccountStatusMock$.next(AuthenticationStatus.Unlocked); }); - it("ignores updating the overlay ciphers if the user's auth status is not unlocked", async () => { + it("skips updating the overlay ciphers if the user's auth status is not unlocked", async () => { activeAccountStatusMock$.next(AuthenticationStatus.Locked); - jest.spyOn(BrowserApi, "getTabFromCurrentWindowId"); - jest.spyOn(cipherService, "getAllDecryptedForUrl"); await overlayBackground.updateOverlayCiphers(); - expect(BrowserApi.getTabFromCurrentWindowId).not.toHaveBeenCalled(); + expect(getTabFromCurrentWindowIdSpy).not.toHaveBeenCalled(); expect(cipherService.getAllDecryptedForUrl).not.toHaveBeenCalled(); }); - it("ignores updating the overlay ciphers if the tab is undefined", async () => { - jest.spyOn(BrowserApi, "getTabFromCurrentWindowId").mockResolvedValueOnce(undefined); - jest.spyOn(cipherService, "getAllDecryptedForUrl"); + it("closes the inline menu on the focused field's tab if the user's auth status is not unlocked", async () => { + activeAccountStatusMock$.next(AuthenticationStatus.Locked); + const previousTab = mock({ id: 1 }); + overlayBackground["focusedFieldData"] = createFocusedFieldDataMock({ tabId: 1 }); + getTabSpy.mockResolvedValueOnce(previousTab); await overlayBackground.updateOverlayCiphers(); - expect(BrowserApi.getTabFromCurrentWindowId).toHaveBeenCalled(); - expect(cipherService.getAllDecryptedForUrl).not.toHaveBeenCalled(); + expect(tabsSendMessageSpy).toHaveBeenCalledWith( + previousTab, + { command: "closeAutofillInlineMenu", overlayElement: undefined }, + { frameId: 0 }, + ); + }); + + it("closes the inline menu on the focused field's tab if current tab is different", async () => { + getTabFromCurrentWindowIdSpy.mockResolvedValueOnce(tab); + cipherService.getAllDecryptedForUrl.mockResolvedValue([cipher1, cipher2]); + cipherService.sortCiphersByLastUsedThenName.mockReturnValue(-1); + const previousTab = mock({ id: 15 }); + overlayBackground["focusedFieldData"] = createFocusedFieldDataMock({ tabId: 15 }); + getTabSpy.mockResolvedValueOnce(previousTab); + + await overlayBackground.updateOverlayCiphers(); + + expect(tabsSendMessageSpy).toHaveBeenCalledWith( + previousTab, + { command: "closeAutofillInlineMenu", overlayElement: undefined }, + { frameId: 0 }, + ); }); it("queries all ciphers for the given url, sort them by last used, and format them for usage in the overlay", async () => { - jest.spyOn(BrowserApi, "getTabFromCurrentWindowId").mockResolvedValueOnce(tab); + getTabFromCurrentWindowIdSpy.mockResolvedValueOnce(tab); cipherService.getAllDecryptedForUrl.mockResolvedValue([cipher1, cipher2]); cipherService.sortCiphersByLastUsedThenName.mockReturnValue(-1); - jest.spyOn(BrowserApi, "tabSendMessageData").mockImplementation(); - jest.spyOn(overlayBackground as any, "getOverlayCipherData"); await overlayBackground.updateOverlayCiphers(); expect(BrowserApi.getTabFromCurrentWindowId).toHaveBeenCalled(); expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledWith(url); - expect(overlayBackground["cipherService"].sortCiphersByLastUsedThenName).toHaveBeenCalled(); - expect(overlayBackground["overlayLoginCiphers"]).toStrictEqual( + expect(cipherService.sortCiphersByLastUsedThenName).toHaveBeenCalled(); + expect(overlayBackground["inlineMenuCiphers"]).toStrictEqual( new Map([ - ["overlay-cipher-0", cipher2], - ["overlay-cipher-1", cipher1], + ["inline-menu-cipher-0", cipher2], + ["inline-menu-cipher-1", cipher1], ]), ); - expect(overlayBackground["getOverlayCipherData"]).toHaveBeenCalled(); }); - it("posts an `updateOverlayListCiphers` message to the overlay list port, and send a `updateIsOverlayCiphersPopulated` message to the tab indicating that the list of ciphers is populated", async () => { - overlayBackground["overlayListPort"] = mock(); + it("posts an `updateOverlayListCiphers` message to the overlay list port, and send a `updateAutofillInlineMenuListCiphers` message to the tab indicating that the list of ciphers is populated", async () => { + overlayBackground["inlineMenuListPort"] = mock(); cipherService.getAllDecryptedForUrl.mockResolvedValue([cipher1, cipher2]); cipherService.sortCiphersByLastUsedThenName.mockReturnValue(-1); - jest.spyOn(BrowserApi, "getTabFromCurrentWindowId").mockResolvedValueOnce(tab); - jest.spyOn(BrowserApi, "tabSendMessageData").mockImplementation(); + getTabFromCurrentWindowIdSpy.mockResolvedValueOnce(tab); await overlayBackground.updateOverlayCiphers(); - expect(overlayBackground["overlayListPort"].postMessage).toHaveBeenCalledWith({ - command: "updateOverlayListCiphers", + expect(overlayBackground["inlineMenuListPort"].postMessage).toHaveBeenCalledWith({ + command: "updateAutofillInlineMenuListCiphers", ciphers: [ { - card: null, + card: cipher2.card.subTitle, favorite: cipher2.favorite, icon: { - fallbackImage: "images/bwi-globe.png", - icon: "bwi-globe", - image: "https://icons.bitwarden.com//jest-testing-website.com/icon.png", + fallbackImage: "", + icon: "bwi-credit-card", + image: undefined, imageEnabled: true, }, - id: "overlay-cipher-0", - login: { - username: "username-2", - }, + id: "inline-menu-cipher-0", + login: null, name: "name-2", reprompt: cipher2.reprompt, - type: 1, + type: 3, }, { card: null, @@ -250,7 +803,7 @@ describe("OverlayBackground", () => { image: "https://icons.bitwarden.com//jest-testing-website.com/icon.png", imageEnabled: true, }, - id: "overlay-cipher-1", + id: "inline-menu-cipher-1", login: { username: "username-1", }, @@ -260,227 +813,822 @@ describe("OverlayBackground", () => { }, ], }); - expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith( - tab, - "updateIsOverlayCiphersPopulated", - { isOverlayCiphersPopulated: true }, - ); }); }); - describe("getOverlayCipherData", () => { - const url = "https://jest-testing-website.com"; - const cipher1 = mock({ - id: "id-1", - localData: { lastUsedDate: 222 }, - name: "name-1", - type: CipherType.Login, - login: { username: "username-1", uri: url }, - }); - const cipher2 = mock({ - id: "id-2", - localData: { lastUsedDate: 111 }, - name: "name-2", - type: CipherType.Login, - login: { username: "username-2", uri: url }, - }); - const cipher3 = mock({ - id: "id-3", - localData: { lastUsedDate: 333 }, - name: "name-3", - type: CipherType.Card, - card: { subTitle: "Visa, *6789" }, - }); - const cipher4 = mock({ - id: "id-4", - localData: { lastUsedDate: 444 }, - name: "name-4", - type: CipherType.Card, - card: { subTitle: "Mastercard, *1234" }, - }); + describe("extension message handlers", () => { + describe("autofillOverlayElementClosed message handler", () => { + beforeEach(async () => { + await initOverlayElementPorts(); + }); - it("formats and returns the cipher data", async () => { - overlayBackground["overlayLoginCiphers"] = new Map([ - ["overlay-cipher-0", cipher2], - ["overlay-cipher-1", cipher1], - ["overlay-cipher-2", cipher3], - ["overlay-cipher-3", cipher4], - ]); + it("disconnects any expired ports if the sender is not from the same page as the most recently focused field", () => { + const port1 = mock(); + const port2 = mock(); + overlayBackground["expiredPorts"] = [port1, port2]; + const sender = mock({ tab: { id: 1 } }); + const focusedFieldData = createFocusedFieldDataMock({ tabId: 2 }); + sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); - const overlayCipherData = await overlayBackground["getOverlayCipherData"](); - - expect(overlayCipherData).toStrictEqual([ - { - card: null, - favorite: cipher2.favorite, - icon: { - fallbackImage: "images/bwi-globe.png", - icon: "bwi-globe", - image: "https://icons.bitwarden.com//jest-testing-website.com/icon.png", - imageEnabled: true, + sendMockExtensionMessage( + { + command: "autofillOverlayElementClosed", + overlayElement: AutofillOverlayElement.Button, }, - id: "overlay-cipher-0", - login: { - username: "username-2", - }, - name: "name-2", - reprompt: cipher2.reprompt, - type: 1, - }, - { - card: null, - favorite: cipher1.favorite, - icon: { - fallbackImage: "images/bwi-globe.png", - icon: "bwi-globe", - image: "https://icons.bitwarden.com//jest-testing-website.com/icon.png", - imageEnabled: true, - }, - id: "overlay-cipher-1", - login: { - username: "username-1", - }, - name: "name-1", - reprompt: cipher1.reprompt, - type: 1, - }, - { - card: "Visa, *6789", - favorite: cipher3.favorite, - icon: { - fallbackImage: "", - icon: "bwi-credit-card", - image: undefined, - imageEnabled: true, - }, - id: "overlay-cipher-2", - login: null, - name: "name-3", - reprompt: cipher3.reprompt, - type: 3, - }, - { - card: "Mastercard, *1234", - favorite: cipher4.favorite, - icon: { - fallbackImage: "", - icon: "bwi-credit-card", - image: undefined, - imageEnabled: true, - }, - id: "overlay-cipher-3", - login: null, - name: "name-4", - reprompt: cipher4.reprompt, - type: 3, - }, - ]); - }); - }); + sender, + ); - describe("getAuthStatus", () => { - it("will update the user's auth status but will not update the overlay ciphers", async () => { - const authStatus = AuthenticationStatus.Unlocked; - overlayBackground["userAuthStatus"] = AuthenticationStatus.Unlocked; - jest.spyOn(overlayBackground["authService"], "getAuthStatus").mockResolvedValue(authStatus); - jest.spyOn(overlayBackground as any, "updateOverlayButtonAuthStatus").mockImplementation(); - jest.spyOn(overlayBackground as any, "updateOverlayCiphers").mockImplementation(); + expect(port1.disconnect).toHaveBeenCalled(); + expect(port2.disconnect).toHaveBeenCalled(); + }); - const status = await overlayBackground["getAuthStatus"](); + it("disconnects the button element port", () => { + sendMockExtensionMessage({ + command: "autofillOverlayElementClosed", + overlayElement: AutofillOverlayElement.Button, + }); - expect(overlayBackground["authService"].getAuthStatus).toHaveBeenCalled(); - expect(overlayBackground["updateOverlayButtonAuthStatus"]).not.toHaveBeenCalled(); - expect(overlayBackground["updateOverlayCiphers"]).not.toHaveBeenCalled(); - expect(overlayBackground["userAuthStatus"]).toBe(authStatus); - expect(status).toBe(authStatus); - }); + expect(buttonPortSpy.disconnect).toHaveBeenCalled(); + }); - it("will update the user's auth status and update the overlay ciphers if the status has been modified", async () => { - const authStatus = AuthenticationStatus.Unlocked; - overlayBackground["userAuthStatus"] = AuthenticationStatus.LoggedOut; - jest.spyOn(overlayBackground["authService"], "getAuthStatus").mockResolvedValue(authStatus); - jest.spyOn(overlayBackground as any, "updateOverlayButtonAuthStatus").mockImplementation(); - jest.spyOn(overlayBackground as any, "updateOverlayCiphers").mockImplementation(); + it("disconnects the list element port", () => { + sendMockExtensionMessage({ + command: "autofillOverlayElementClosed", + overlayElement: AutofillOverlayElement.List, + }); - await overlayBackground["getAuthStatus"](); - - expect(overlayBackground["authService"].getAuthStatus).toHaveBeenCalled(); - expect(overlayBackground["updateOverlayButtonAuthStatus"]).toHaveBeenCalled(); - expect(overlayBackground["updateOverlayCiphers"]).toHaveBeenCalled(); - expect(overlayBackground["userAuthStatus"]).toBe(authStatus); - }); - }); - - describe("updateOverlayButtonAuthStatus", () => { - it("will send a message to the button port with the user's auth status", () => { - overlayBackground["overlayButtonPort"] = mock(); - jest.spyOn(overlayBackground["overlayButtonPort"], "postMessage"); - - overlayBackground["updateOverlayButtonAuthStatus"](); - - expect(overlayBackground["overlayButtonPort"].postMessage).toHaveBeenCalledWith({ - command: "updateOverlayButtonAuthStatus", - authStatus: overlayBackground["userAuthStatus"], + expect(listPortSpy.disconnect).toHaveBeenCalled(); }); }); - }); - describe("getTranslations", () => { - it("will query the overlay page translations if they have not been queried", () => { - overlayBackground["overlayPageTranslations"] = undefined; - jest.spyOn(overlayBackground as any, "getTranslations"); - jest.spyOn(overlayBackground["i18nService"], "translate").mockImplementation((key) => key); - jest.spyOn(BrowserApi, "getUILanguage").mockReturnValue("en"); + describe("autofillOverlayAddNewVaultItem message handler", () => { + let sender: chrome.runtime.MessageSender; + let openAddEditVaultItemPopoutSpy: jest.SpyInstance; - const translations = overlayBackground["getTranslations"](); + beforeEach(() => { + sender = mock({ tab: { id: 1 } }); + openAddEditVaultItemPopoutSpy = jest + .spyOn(overlayBackground as any, "openAddEditVaultItemPopout") + .mockImplementation(); + }); - expect(overlayBackground["getTranslations"]).toHaveBeenCalled(); - const translationKeys = [ - "opensInANewWindow", - "bitwardenOverlayButton", - "toggleBitwardenVaultOverlay", - "bitwardenVault", - "unlockYourAccountToViewMatchingLogins", - "unlockAccount", - "fillCredentialsFor", - "partialUsername", - "view", - "noItemsToShow", - "newItem", - "addNewVaultItem", + it("will not open the add edit popout window if the message does not have a login cipher provided", () => { + sendMockExtensionMessage({ command: "autofillOverlayAddNewVaultItem" }, sender); + + expect(cipherService.setAddEditCipherInfo).not.toHaveBeenCalled(); + expect(openAddEditVaultItemPopoutSpy).not.toHaveBeenCalled(); + }); + + it("will open the add edit popout window after creating a new cipher", async () => { + sendMockExtensionMessage( + { + command: "autofillOverlayAddNewVaultItem", + login: { + uri: "https://tacos.com", + hostname: "", + username: "username", + password: "password", + }, + }, + sender, + ); + await flushPromises(); + + expect(cipherService.setAddEditCipherInfo).toHaveBeenCalled(); + expect(sendMessageSpy).toHaveBeenCalledWith("inlineAutofillMenuRefreshAddEditCipher"); + expect(openAddEditVaultItemPopoutSpy).toHaveBeenCalled(); + }); + }); + + describe("checkIsInlineMenuCiphersPopulated message handler", () => { + let focusedFieldData: FocusedFieldData; + + beforeEach(() => { + focusedFieldData = createFocusedFieldDataMock(); + sendMockExtensionMessage( + { command: "updateFocusedFieldData", focusedFieldData }, + mock({ tab: { id: 2 }, frameId: 0 }), + ); + }); + + it("returns false if the sender's tab id is not equal to the focused field's tab id", async () => { + const sender = mock({ tab: { id: 1 } }); + + sendMockExtensionMessage( + { command: "checkIsInlineMenuCiphersPopulated" }, + sender, + sendResponse, + ); + await flushPromises(); + + expect(sendResponse).toHaveBeenCalledWith(false); + }); + + it("returns false if the overlay login cipher are not populated", () => {}); + + it("returns true if the overlay login ciphers are populated", async () => { + overlayBackground["inlineMenuCiphers"] = new Map([ + ["inline-menu-cipher-0", mock()], + ]); + + sendMockExtensionMessage( + { command: "checkIsInlineMenuCiphersPopulated" }, + mock({ tab: { id: 2 } }), + sendResponse, + ); + await flushPromises(); + + expect(sendResponse).toHaveBeenCalledWith(true); + }); + }); + + describe("updateFocusedFieldData message handler", () => { + it("sends a message to the sender frame to unset the most recently focused field data when the currently focused field does not belong to the sender", async () => { + const tab = createChromeTabMock({ id: 2 }); + const firstSender = mock({ tab, frameId: 100 }); + const focusedFieldData = createFocusedFieldDataMock({ + tabId: tab.id, + frameId: firstSender.frameId, + }); + sendMockExtensionMessage( + { command: "updateFocusedFieldData", focusedFieldData }, + firstSender, + ); + await flushPromises(); + + const secondSender = mock({ tab, frameId: 10 }); + const otherFocusedFieldData = createFocusedFieldDataMock({ + tabId: tab.id, + frameId: secondSender.frameId, + }); + sendMockExtensionMessage( + { command: "updateFocusedFieldData", focusedFieldData: otherFocusedFieldData }, + secondSender, + ); + await flushPromises(); + + expect(tabsSendMessageSpy).toHaveBeenCalledWith( + tab, + { command: "unsetMostRecentlyFocusedField" }, + { frameId: firstSender.frameId }, + ); + }); + }); + + describe("checkIsFieldCurrentlyFocused message handler", () => { + it("returns true when a form field is currently focused", async () => { + sendMockExtensionMessage({ + command: "updateIsFieldCurrentlyFocused", + isFieldCurrentlyFocused: true, + }); + + sendMockExtensionMessage( + { command: "checkIsFieldCurrentlyFocused" }, + mock(), + sendResponse, + ); + await flushPromises(); + + expect(sendResponse).toHaveBeenCalledWith(true); + }); + }); + + describe("checkIsFieldCurrentlyFilling message handler", () => { + it("returns true if autofill is currently running", async () => { + sendMockExtensionMessage({ + command: "updateIsFieldCurrentlyFilling", + isFieldCurrentlyFilling: true, + }); + + sendMockExtensionMessage( + { command: "checkIsFieldCurrentlyFilling" }, + mock(), + sendResponse, + ); + await flushPromises(); + + expect(sendResponse).toHaveBeenCalledWith(true); + }); + }); + + describe("getAutofillInlineMenuVisibility message handler", () => { + it("returns the current inline menu visibility setting", async () => { + sendMockExtensionMessage( + { command: "getAutofillInlineMenuVisibility" }, + mock(), + sendResponse, + ); + await flushPromises(); + + expect(sendResponse).toHaveBeenCalledWith(AutofillOverlayVisibility.OnFieldFocus); + }); + }); + + describe("openAutofillInlineMenu message handler", () => { + let sender: chrome.runtime.MessageSender; + + beforeEach(() => { + sender = mock({ tab: { id: 1 } }); + getTabFromCurrentWindowIdSpy.mockResolvedValue(sender.tab); + tabsSendMessageSpy.mockImplementation(); + }); + + it("opens the autofill inline menu by sending a message to the current tab", async () => { + sendMockExtensionMessage({ command: "openAutofillInlineMenu" }, sender); + await flushPromises(); + + expect(tabsSendMessageSpy).toHaveBeenCalledWith( + sender.tab, + { + command: "openAutofillInlineMenu", + isFocusingFieldElement: false, + isOpeningFullInlineMenu: false, + authStatus: AuthenticationStatus.Unlocked, + }, + { frameId: 0 }, + ); + }); + + it("sends the open menu message to the focused field's frameId", async () => { + sender.frameId = 10; + sendMockExtensionMessage({ command: "updateFocusedFieldData" }, sender); + await flushPromises(); + + sendMockExtensionMessage({ command: "openAutofillInlineMenu" }, sender); + await flushPromises(); + + expect(tabsSendMessageSpy).toHaveBeenCalledWith( + sender.tab, + { + command: "openAutofillInlineMenu", + isFocusingFieldElement: false, + isOpeningFullInlineMenu: false, + authStatus: AuthenticationStatus.Unlocked, + }, + { frameId: 10 }, + ); + }); + }); + + describe("closeAutofillInlineMenu", () => { + let sender: chrome.runtime.MessageSender; + + beforeEach(() => { + sender = mock({ tab: { id: 1 } }); + sendMockExtensionMessage({ + command: "updateIsFieldCurrentlyFilling", + isFieldCurrentlyFilling: false, + }); + sendMockExtensionMessage({ + command: "updateIsFieldCurrentlyFocused", + isFieldCurrentlyFocused: false, + }); + }); + + it("sends a message to close the inline menu without checking field focus state if forcing the closure", async () => { + sendMockExtensionMessage({ + command: "updateIsFieldCurrentlyFocused", + isFieldCurrentlyFocused: true, + }); + await flushPromises(); + + sendMockExtensionMessage( + { + command: "closeAutofillInlineMenu", + forceCloseInlineMenu: true, + overlayElement: AutofillOverlayElement.Button, + }, + sender, + ); + await flushPromises(); + + expect(tabsSendMessageSpy).toHaveBeenCalledWith( + sender.tab, + { + command: "closeAutofillInlineMenu", + overlayElement: AutofillOverlayElement.Button, + }, + { frameId: 0 }, + ); + }); + + it("skips sending a message to close the inline menu if a form field is currently focused", async () => { + sendMockExtensionMessage({ + command: "updateIsFieldCurrentlyFocused", + isFieldCurrentlyFocused: true, + }); + await flushPromises(); + + sendMockExtensionMessage( + { + command: "closeAutofillInlineMenu", + forceCloseInlineMenu: false, + overlayElement: AutofillOverlayElement.Button, + }, + sender, + ); + await flushPromises(); + + expect(tabsSendMessageSpy).not.toHaveBeenCalled(); + }); + + it("sends a message to close the inline menu list only if the field is currently filling", async () => { + sendMockExtensionMessage({ + command: "updateIsFieldCurrentlyFilling", + isFieldCurrentlyFilling: true, + }); + await flushPromises(); + + sendMockExtensionMessage({ command: "closeAutofillInlineMenu" }, sender); + await flushPromises(); + + expect(tabsSendMessageSpy).toHaveBeenCalledWith( + sender.tab, + { + command: "closeAutofillInlineMenu", + overlayElement: AutofillOverlayElement.List, + }, + { frameId: 0 }, + ); + expect(tabsSendMessageSpy).not.toHaveBeenCalledWith( + sender.tab, + { + command: "closeAutofillInlineMenu", + overlayElement: AutofillOverlayElement.Button, + }, + { frameId: 0 }, + ); + }); + + it("sends a message to close the inline menu if the form field is not focused and not filling", async () => { + overlayBackground["isInlineMenuButtonVisible"] = true; + overlayBackground["isInlineMenuListVisible"] = true; + + sendMockExtensionMessage({ command: "closeAutofillInlineMenu" }, sender); + await flushPromises(); + + expect(tabsSendMessageSpy).toHaveBeenCalledWith( + sender.tab, + { + command: "closeAutofillInlineMenu", + overlayElement: undefined, + }, + { frameId: 0 }, + ); + expect(overlayBackground["isInlineMenuButtonVisible"]).toBe(false); + expect(overlayBackground["isInlineMenuListVisible"]).toBe(false); + }); + + it("sets a property indicating that the inline menu button is not visible", async () => { + overlayBackground["isInlineMenuButtonVisible"] = true; + + sendMockExtensionMessage( + { command: "closeAutofillInlineMenu", overlayElement: AutofillOverlayElement.Button }, + sender, + ); + await flushPromises(); + + expect(overlayBackground["isInlineMenuButtonVisible"]).toBe(false); + }); + + it("sets a property indicating that the inline menu list is not visible", async () => { + overlayBackground["isInlineMenuListVisible"] = true; + + sendMockExtensionMessage( + { command: "closeAutofillInlineMenu", overlayElement: AutofillOverlayElement.List }, + sender, + ); + await flushPromises(); + + expect(overlayBackground["isInlineMenuListVisible"]).toBe(false); + }); + }); + + describe("checkAutofillInlineMenuFocused message handler", () => { + beforeEach(async () => { + await initOverlayElementPorts(); + }); + + it("skips checking if the inline menu is focused if the sender does not contain the focused field", async () => { + const sender = mock({ tab: { id: 1 } }); + const focusedFieldData = createFocusedFieldDataMock(); + sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); + + sendMockExtensionMessage({ command: "checkAutofillInlineMenuFocused" }, sender); + + expect(listPortSpy.postMessage).not.toHaveBeenCalledWith({ + command: "checkAutofillInlineMenuListFocused", + }); + expect(buttonPortSpy.postMessage).not.toHaveBeenCalledWith({ + command: "checkAutofillInlineMenuButtonFocused", + }); + }); + + it("will check if the inline menu list is focused if the list port is open", () => { + sendMockExtensionMessage({ command: "checkAutofillInlineMenuFocused" }); + + expect(listPortSpy.postMessage).toHaveBeenCalledWith({ + command: "checkAutofillInlineMenuListFocused", + }); + expect(buttonPortSpy.postMessage).not.toHaveBeenCalledWith({ + command: "checkAutofillInlineMenuButtonFocused", + }); + }); + + it("will check if the overlay button is focused if the list port is not open", () => { + overlayBackground["inlineMenuListPort"] = undefined; + + sendMockExtensionMessage({ command: "checkAutofillInlineMenuFocused" }); + + expect(buttonPortSpy.postMessage).toHaveBeenCalledWith({ + command: "checkAutofillInlineMenuButtonFocused", + }); + expect(listPortSpy.postMessage).not.toHaveBeenCalledWith({ + command: "checkAutofillInlineMenuListFocused", + }); + }); + }); + + describe("focusAutofillInlineMenuList message handler", () => { + it("will send a `focusInlineMenuList` message to the overlay list port", async () => { + await initOverlayElementPorts({ initList: true, initButton: false }); + + sendMockExtensionMessage({ command: "focusAutofillInlineMenuList" }); + + expect(listPortSpy.postMessage).toHaveBeenCalledWith({ + command: "focusAutofillInlineMenuList", + }); + }); + }); + + describe("updateAutofillInlineMenuPosition message handler", () => { + beforeEach(async () => { + await initOverlayElementPorts(); + }); + + it("ignores updating the position if the overlay element type is not provided", () => { + sendMockExtensionMessage({ command: "updateAutofillInlineMenuPosition" }); + + expect(listPortSpy.postMessage).not.toHaveBeenCalledWith({ + command: "updateIframePosition", + styles: expect.anything(), + }); + expect(buttonPortSpy.postMessage).not.toHaveBeenCalledWith({ + command: "updateIframePosition", + styles: expect.anything(), + }); + }); + + it("skips updating the position if the most recently focused field is different than the message sender", () => { + const sender = mock({ tab: { id: 1 } }); + const focusedFieldData = createFocusedFieldDataMock({ tabId: 2 }); + sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); + + sendMockExtensionMessage({ command: "updateAutofillInlineMenuPosition" }, sender); + + expect(listPortSpy.postMessage).not.toHaveBeenCalledWith({ + command: "updateIframePosition", + styles: expect.anything(), + }); + expect(buttonPortSpy.postMessage).not.toHaveBeenCalledWith({ + command: "updateIframePosition", + styles: expect.anything(), + }); + }); + + it("updates the inline menu button's position", async () => { + const focusedFieldData = createFocusedFieldDataMock(); + sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); + + sendMockExtensionMessage({ + command: "updateAutofillInlineMenuPosition", + overlayElement: AutofillOverlayElement.Button, + }); + await flushPromises(); + + expect(buttonPortSpy.postMessage).toHaveBeenCalledWith({ + command: "updateAutofillInlineMenuPosition", + styles: { height: "2px", left: "4px", top: "2px", width: "2px" }, + }); + }); + + it("modifies the inline menu button's height for medium sized input elements", async () => { + const focusedFieldData = createFocusedFieldDataMock({ + focusedFieldRects: { top: 1, left: 2, height: 35, width: 4 }, + }); + sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); + + sendMockExtensionMessage({ + command: "updateAutofillInlineMenuPosition", + overlayElement: AutofillOverlayElement.Button, + }); + await flushPromises(); + + expect(buttonPortSpy.postMessage).toHaveBeenCalledWith({ + command: "updateAutofillInlineMenuPosition", + styles: { height: "20px", left: "-22px", top: "8px", width: "20px" }, + }); + }); + + it("modifies the inline menu button's height for large sized input elements", async () => { + const focusedFieldData = createFocusedFieldDataMock({ + focusedFieldRects: { top: 1, left: 2, height: 50, width: 4 }, + }); + sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); + + sendMockExtensionMessage({ + command: "updateAutofillInlineMenuPosition", + overlayElement: AutofillOverlayElement.Button, + }); + await flushPromises(); + + expect(buttonPortSpy.postMessage).toHaveBeenCalledWith({ + command: "updateAutofillInlineMenuPosition", + styles: { height: "27px", left: "-32px", top: "13px", width: "27px" }, + }); + }); + + it("takes into account the right padding of the focused field in positioning the button if the right padding of the field is larger than the left padding", async () => { + const focusedFieldData = createFocusedFieldDataMock({ + focusedFieldStyles: { paddingRight: "20px", paddingLeft: "6px" }, + }); + sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); + + sendMockExtensionMessage({ + command: "updateAutofillInlineMenuPosition", + overlayElement: AutofillOverlayElement.Button, + }); + await flushPromises(); + + expect(buttonPortSpy.postMessage).toHaveBeenCalledWith({ + command: "updateAutofillInlineMenuPosition", + styles: { height: "2px", left: "-18px", top: "2px", width: "2px" }, + }); + }); + + it("updates the inline menu list's position", async () => { + const focusedFieldData = createFocusedFieldDataMock(); + sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); + + sendMockExtensionMessage({ + command: "updateAutofillInlineMenuPosition", + overlayElement: AutofillOverlayElement.List, + }); + await flushPromises(); + + expect(listPortSpy.postMessage).toHaveBeenCalledWith({ + command: "updateAutofillInlineMenuPosition", + styles: { left: "2px", top: "4px", width: "4px" }, + }); + }); + + it("sends a message that triggers a simultaneous fade in for both inline menu elements", async () => { + jest.useFakeTimers(); + const focusedFieldData = createFocusedFieldDataMock(); + sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); + + sendMockExtensionMessage({ + command: "updateAutofillInlineMenuPosition", + overlayElement: AutofillOverlayElement.List, + }); + await flushPromises(); + jest.advanceTimersByTime(150); + + expect(buttonPortSpy.postMessage).toHaveBeenCalledWith({ + command: "fadeInAutofillInlineMenuIframe", + }); + expect(listPortSpy.postMessage).toHaveBeenCalledWith({ + command: "fadeInAutofillInlineMenuIframe", + }); + }); + + it("triggers a debounced reposition of the inline menu if the sender frame has a `null` sub frame offsets value", async () => { + jest.useFakeTimers(); + const focusedFieldData = createFocusedFieldDataMock(); + const sender = mock({ + tab: { id: focusedFieldData.tabId }, + frameId: focusedFieldData.frameId, + }); + sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }, sender); + overlayBackground["subFrameOffsetsForTab"][focusedFieldData.tabId] = new Map([ + [focusedFieldData.frameId, null], + ]); + tabsSendMessageSpy.mockImplementation(); + jest.spyOn(overlayBackground as any, "updateInlineMenuPositionAfterRepositionEvent"); + + sendMockExtensionMessage( + { + command: "updateAutofillInlineMenuPosition", + overlayElement: AutofillOverlayElement.List, + }, + sender, + ); + await flushPromises(); + jest.advanceTimersByTime(150); + + expect( + overlayBackground["updateInlineMenuPositionAfterRepositionEvent"], + ).toHaveBeenCalled(); + }); + }); + + describe("updateAutofillInlineMenuElementIsVisibleStatus message handler", () => { + let sender: chrome.runtime.MessageSender; + let focusedFieldData: FocusedFieldData; + + beforeEach(() => { + sender = mock({ tab: { id: 1 } }); + focusedFieldData = createFocusedFieldDataMock(); + overlayBackground["isInlineMenuButtonVisible"] = true; + overlayBackground["isInlineMenuListVisible"] = false; + }); + + it("skips updating the inline menu visibility status if the sender tab does not contain the focused field", async () => { + const otherSender = mock({ tab: { id: 2 } }); + sendMockExtensionMessage( + { command: "updateFocusedFieldData", focusedFieldData }, + otherSender, + ); + + sendMockExtensionMessage( + { + command: "updateAutofillInlineMenuElementIsVisibleStatus", + overlayElement: AutofillOverlayElement.Button, + isVisible: false, + }, + sender, + ); + + expect(overlayBackground["isInlineMenuButtonVisible"]).toBe(true); + expect(overlayBackground["isInlineMenuListVisible"]).toBe(false); + }); + + it("updates the visibility status of the inline menu button", async () => { + sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }, sender); + + sendMockExtensionMessage( + { + command: "updateAutofillInlineMenuElementIsVisibleStatus", + overlayElement: AutofillOverlayElement.Button, + isVisible: false, + }, + sender, + ); + + expect(overlayBackground["isInlineMenuButtonVisible"]).toBe(false); + expect(overlayBackground["isInlineMenuListVisible"]).toBe(false); + }); + + it("updates the visibility status of the inline menu list", async () => { + sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }, sender); + + sendMockExtensionMessage( + { + command: "updateAutofillInlineMenuElementIsVisibleStatus", + overlayElement: AutofillOverlayElement.List, + isVisible: true, + }, + sender, + ); + + expect(overlayBackground["isInlineMenuButtonVisible"]).toBe(true); + expect(overlayBackground["isInlineMenuListVisible"]).toBe(true); + }); + }); + + describe("checkIsAutofillInlineMenuButtonVisible message handler", () => { + it("returns true when the inline menu button is visible", async () => { + overlayBackground["isInlineMenuButtonVisible"] = true; + const sender = mock({ tab: { id: 1 } }); + + sendMockExtensionMessage( + { command: "checkIsAutofillInlineMenuButtonVisible" }, + sender, + sendResponse, + ); + await flushPromises(); + + expect(sendResponse).toHaveBeenCalledWith(true); + }); + }); + + describe("checkIsAutofillInlineMenuListVisible message handler", () => { + it("returns true when the inline menu list is visible", async () => { + overlayBackground["isInlineMenuListVisible"] = true; + const sender = mock({ tab: { id: 1 } }); + + sendMockExtensionMessage( + { command: "checkIsAutofillInlineMenuListVisible" }, + sender, + sendResponse, + ); + await flushPromises(); + + expect(sendResponse).toHaveBeenCalledWith(true); + }); + }); + + describe("getCurrentTabFrameId message handler", () => { + it("returns the sender's frame id", async () => { + const sender = mock({ frameId: 1 }); + + sendMockExtensionMessage({ command: "getCurrentTabFrameId" }, sender, sendResponse); + await flushPromises(); + + expect(sendResponse).toHaveBeenCalledWith(1); + }); + }); + + describe("destroyAutofillInlineMenuListeners", () => { + it("sends a message to the passed frameId that triggers a destruction of the inline menu listeners on that frame", () => { + const sender = mock({ tab: { id: 1 }, frameId: 0 }); + + sendMockExtensionMessage( + { command: "destroyAutofillInlineMenuListeners", subFrameData: { frameId: 10 } }, + sender, + ); + + expect(tabsSendMessageSpy).toHaveBeenCalledWith( + sender.tab, + { command: "destroyAutofillInlineMenuListeners" }, + { frameId: 10 }, + ); + }); + }); + + describe("unlockCompleted", () => { + let updateInlineMenuCiphersSpy: jest.SpyInstance; + + beforeEach(async () => { + updateInlineMenuCiphersSpy = jest.spyOn(overlayBackground, "updateOverlayCiphers"); + await initOverlayElementPorts(); + }); + + it("updates the inline menu button auth status", async () => { + sendMockExtensionMessage({ command: "unlockCompleted" }); + await flushPromises(); + + expect(buttonPortSpy.postMessage).toHaveBeenCalledWith({ + command: "updateInlineMenuButtonAuthStatus", + authStatus: AuthenticationStatus.Unlocked, + }); + }); + + it("updates the overlay ciphers", async () => { + const updateInlineMenuCiphersSpy = jest.spyOn(overlayBackground, "updateOverlayCiphers"); + sendMockExtensionMessage({ command: "unlockCompleted" }); + await flushPromises(); + + expect(updateInlineMenuCiphersSpy).toHaveBeenCalled(); + }); + + it("opens the inline menu if a retry command is present in the message", async () => { + updateInlineMenuCiphersSpy.mockImplementation(); + getTabFromCurrentWindowIdSpy.mockResolvedValueOnce(createChromeTabMock({ id: 1 })); + sendMockExtensionMessage({ + command: "unlockCompleted", + data: { + commandToRetry: { message: { command: "openAutofillInlineMenu" } }, + }, + }); + await flushPromises(); + + expect(tabsSendMessageSpy).toHaveBeenCalledWith( + expect.any(Object), + { + command: "openAutofillInlineMenu", + isFocusingFieldElement: true, + isOpeningFullInlineMenu: false, + authStatus: AuthenticationStatus.Unlocked, + }, + { frameId: 0 }, + ); + }); + }); + + describe("extension messages that trigger an update of the inline menu ciphers", () => { + const extensionMessages = [ + "doFullSync", + "addedCipher", + "addEditCipherSubmitted", + "editedCipher", + "deletedCipher", ]; - translationKeys.forEach((key) => { - expect(overlayBackground["i18nService"].translate).toHaveBeenCalledWith(key); + + beforeEach(() => { + jest.spyOn(overlayBackground, "updateOverlayCiphers").mockImplementation(); }); - expect(translations).toStrictEqual({ - locale: "en", - opensInANewWindow: "opensInANewWindow", - buttonPageTitle: "bitwardenOverlayButton", - toggleBitwardenVaultOverlay: "toggleBitwardenVaultOverlay", - listPageTitle: "bitwardenVault", - unlockYourAccount: "unlockYourAccountToViewMatchingLogins", - unlockAccount: "unlockAccount", - fillCredentialsFor: "fillCredentialsFor", - partialUsername: "partialUsername", - view: "view", - noItemsToShow: "noItemsToShow", - newItem: "newItem", - addNewVaultItem: "addNewVaultItem", + + extensionMessages.forEach((message) => { + it(`triggers an update of the overlay ciphers when the ${message} message is received`, () => { + sendMockExtensionMessage({ command: message }); + expect(overlayBackground.updateOverlayCiphers).toHaveBeenCalled(); + }); }); }); }); - describe("setupExtensionMessageListeners", () => { - it("will set up onMessage and onConnect listeners", () => { - overlayBackground["setupExtensionMessageListeners"](); - - // eslint-disable-next-line - expect(chrome.runtime.onMessage.addListener).toHaveBeenCalled(); - expect(chrome.runtime.onConnect.addListener).toHaveBeenCalled(); - }); - }); - - describe("handleExtensionMessage", () => { + describe("handle extension onMessage", () => { it("will return early if the message command is not present within the extensionMessageHandlers", () => { const message = { command: "not-a-command", @@ -494,970 +1642,591 @@ describe("OverlayBackground", () => { sendResponse, ); - expect(returnValue).toBe(undefined); + expect(returnValue).toBe(null); expect(sendResponse).not.toHaveBeenCalled(); }); + }); - it("will trigger the message handler and return undefined if the message does not have a response", () => { - const message = { - command: "autofillOverlayElementClosed", - }; - const sender = mock({ tab: { id: 1 } }); - const sendResponse = jest.fn(); - jest.spyOn(overlayBackground as any, "overlayElementClosed"); + describe("inline menu button message handlers", () => { + let sender: chrome.runtime.MessageSender; + const portKey = "inlineMenuButtonPort"; - const returnValue = overlayBackground["handleExtensionMessage"]( - message, - sender, - sendResponse, - ); - - expect(returnValue).toBe(undefined); - expect(sendResponse).not.toHaveBeenCalled(); - expect(overlayBackground["overlayElementClosed"]).toHaveBeenCalledWith(message, sender); + beforeEach(async () => { + sender = mock({ tab: { id: 1 } }); + portKeyForTabSpy[sender.tab.id] = portKey; + activeAccountStatusMock$.next(AuthenticationStatus.Unlocked); + await initOverlayElementPorts(); + buttonMessageConnectorSpy.sender = sender; + openUnlockPopoutSpy.mockImplementation(); }); - it("will return a response if the message handler returns a response", async () => { - const message = { - command: "openAutofillOverlay", - }; - const sender = mock({ tab: { id: 1 } }); - const sendResponse = jest.fn(); - jest.spyOn(overlayBackground as any, "getTranslations").mockReturnValue("translations"); + describe("autofillInlineMenuButtonClicked message handler", () => { + it("opens the unlock vault popout if the user auth status is not unlocked", async () => { + activeAccountStatusMock$.next(AuthenticationStatus.Locked); + tabsSendMessageSpy.mockImplementation(); - const returnValue = overlayBackground["handleExtensionMessage"]( - message, - sender, - sendResponse, - ); + sendPortMessage(buttonMessageConnectorSpy, { + command: "autofillInlineMenuButtonClicked", + portKey, + }); + await flushPromises(); - expect(returnValue).toBe(true); + expect(tabsSendMessageSpy).toHaveBeenCalledWith( + sender.tab, + { command: "closeAutofillInlineMenu", overlayElement: undefined }, + { frameId: 0 }, + ); + expect(tabSendMessageDataSpy).toBeCalledWith( + sender.tab, + "addToLockedVaultPendingNotifications", + { + commandToRetry: { message: { command: "openAutofillInlineMenu" }, sender }, + target: "overlay.background", + }, + ); + expect(openUnlockPopoutSpy).toHaveBeenCalled(); + }); + + it("opens the inline menu if the user auth status is unlocked", async () => { + getTabFromCurrentWindowIdSpy.mockResolvedValueOnce(sender.tab); + sendPortMessage(buttonMessageConnectorSpy, { + command: "autofillInlineMenuButtonClicked", + portKey, + }); + await flushPromises(); + + expect(tabsSendMessageSpy).toHaveBeenCalledWith( + sender.tab, + { + command: "openAutofillInlineMenu", + isFocusingFieldElement: false, + isOpeningFullInlineMenu: true, + authStatus: AuthenticationStatus.Unlocked, + }, + { frameId: 0 }, + ); + }); }); - describe("extension message handlers", () => { - beforeEach(() => { - jest - .spyOn(overlayBackground as any, "getAuthStatus") - .mockResolvedValue(AuthenticationStatus.Unlocked); + describe("triggerDelayedAutofillInlineMenuClosure message handler", () => { + it("skips triggering the delayed closure of the inline menu if a field is currently focused", async () => { + jest.useFakeTimers(); + sendMockExtensionMessage({ + command: "updateIsFieldCurrentlyFocused", + isFieldCurrentlyFocused: true, + }); + await flushPromises(); + + sendPortMessage(buttonMessageConnectorSpy, { + command: "triggerDelayedAutofillInlineMenuClosure", + portKey, + }); + await flushPromises(); + jest.advanceTimersByTime(100); + + const message = { command: "triggerDelayedAutofillInlineMenuClosure" }; + expect(buttonPortSpy.postMessage).not.toHaveBeenCalledWith(message); + expect(listPortSpy.postMessage).not.toHaveBeenCalledWith(message); }); - describe("openAutofillOverlay message handler", () => { - it("opens the autofill overlay by sending a message to the current tab", async () => { - const sender = mock({ tab: { id: 1 } }); - jest.spyOn(BrowserApi, "getTabFromCurrentWindowId").mockResolvedValueOnce(sender.tab); - jest.spyOn(BrowserApi, "tabSendMessageData").mockImplementation(); - - sendMockExtensionMessage({ command: "openAutofillOverlay" }); - await flushPromises(); - - expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith( - sender.tab, - "openAutofillOverlay", - { - isFocusingFieldElement: false, - isOpeningFullOverlay: false, - authStatus: AuthenticationStatus.Unlocked, - }, - ); + it("sends a message to the button and list ports that triggers a delayed closure of the inline menu", async () => { + jest.useFakeTimers(); + sendPortMessage(buttonMessageConnectorSpy, { + command: "triggerDelayedAutofillInlineMenuClosure", + portKey, }); + await flushPromises(); + jest.advanceTimersByTime(100); + + const message = { command: "triggerDelayedAutofillInlineMenuClosure" }; + expect(buttonPortSpy.postMessage).toHaveBeenCalledWith(message); + expect(listPortSpy.postMessage).toHaveBeenCalledWith(message); }); - describe("autofillOverlayElementClosed message handler", () => { - beforeEach(async () => { - await initOverlayElementPorts(); + it("triggers a single delayed closure if called again within a 100ms threshold", async () => { + jest.useFakeTimers(); + sendPortMessage(buttonMessageConnectorSpy, { + command: "triggerDelayedAutofillInlineMenuClosure", + portKey, }); - - it("disconnects any expired ports if the sender is not from the same page as the most recently focused field", () => { - const port1 = mock(); - const port2 = mock(); - overlayBackground["expiredPorts"] = [port1, port2]; - const sender = mock({ tab: { id: 1 } }); - const focusedFieldData = createFocusedFieldDataMock({ tabId: 2 }); - sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); - - sendMockExtensionMessage( - { - command: "autofillOverlayElementClosed", - overlayElement: AutofillOverlayElement.Button, - }, - sender, - ); - - expect(port1.disconnect).toHaveBeenCalled(); - expect(port2.disconnect).toHaveBeenCalled(); + await flushPromises(); + jest.advanceTimersByTime(50); + sendPortMessage(buttonMessageConnectorSpy, { + command: "triggerDelayedAutofillInlineMenuClosure", + portKey, }); + await flushPromises(); + jest.advanceTimersByTime(100); - it("disconnects the button element port", () => { - sendMockExtensionMessage({ - command: "autofillOverlayElementClosed", - overlayElement: AutofillOverlayElement.Button, - }); + const message = { command: "triggerDelayedAutofillInlineMenuClosure" }; + expect(buttonPortSpy.postMessage).toHaveBeenCalledTimes(2); + expect(buttonPortSpy.postMessage).not.toHaveBeenNthCalledWith(1, message); + expect(buttonPortSpy.postMessage).toHaveBeenNthCalledWith(2, message); + expect(listPortSpy.postMessage).toHaveBeenCalledTimes(2); + expect(listPortSpy.postMessage).not.toHaveBeenNthCalledWith(1, message); + expect(listPortSpy.postMessage).toHaveBeenNthCalledWith(2, message); + }); + }); - expect(buttonPortSpy.disconnect).toHaveBeenCalled(); - expect(overlayBackground["overlayButtonPort"]).toBeNull(); + describe("autofillInlineMenuBlurred message handler", () => { + it("sends a message to the inline menu list to check if the element is focused", async () => { + sendPortMessage(buttonMessageConnectorSpy, { + command: "autofillInlineMenuBlurred", + portKey, }); + await flushPromises(); - it("disconnects the list element port", () => { - sendMockExtensionMessage({ - command: "autofillOverlayElementClosed", - overlayElement: AutofillOverlayElement.List, - }); - - expect(listPortSpy.disconnect).toHaveBeenCalled(); - expect(overlayBackground["overlayListPort"]).toBeNull(); + expect(listPortSpy.postMessage).toHaveBeenCalledWith({ + command: "checkAutofillInlineMenuListFocused", }); }); + }); - describe("autofillOverlayAddNewVaultItem message handler", () => { - let sender: chrome.runtime.MessageSender; - beforeEach(() => { - sender = mock({ tab: { id: 1 } }); - jest - .spyOn(overlayBackground["cipherService"], "setAddEditCipherInfo") - .mockImplementation(); - jest.spyOn(overlayBackground as any, "openAddEditVaultItemPopout").mockImplementation(); + describe("redirectAutofillInlineMenuFocusOut message handler", () => { + it("ignores the redirect message if the direction is not provided", () => { + sendPortMessage(buttonMessageConnectorSpy, { + command: "redirectAutofillInlineMenuFocusOut", + portKey, }); - it("will not open the add edit popout window if the message does not have a login cipher provided", () => { - sendMockExtensionMessage({ command: "autofillOverlayAddNewVaultItem" }, sender); - - expect(overlayBackground["cipherService"].setAddEditCipherInfo).not.toHaveBeenCalled(); - expect(overlayBackground["openAddEditVaultItemPopout"]).not.toHaveBeenCalled(); - }); - - it("will open the add edit popout window after creating a new cipher", async () => { - jest.spyOn(BrowserApi, "sendMessage"); - - sendMockExtensionMessage( - { - command: "autofillOverlayAddNewVaultItem", - login: { - uri: "https://tacos.com", - hostname: "", - username: "username", - password: "password", - }, - }, - sender, - ); - await flushPromises(); - - expect(overlayBackground["cipherService"].setAddEditCipherInfo).toHaveBeenCalled(); - expect(BrowserApi.sendMessage).toHaveBeenCalledWith( - "inlineAutofillMenuRefreshAddEditCipher", - ); - expect(overlayBackground["openAddEditVaultItemPopout"]).toHaveBeenCalled(); - }); + expect(tabSendMessageDataSpy).not.toHaveBeenCalled(); }); - describe("getAutofillOverlayVisibility message handler", () => { - beforeEach(() => { - jest - .spyOn(overlayBackground as any, "getOverlayVisibility") - .mockResolvedValue(AutofillOverlayVisibility.OnFieldFocus); + it("sends the redirect message if the direction is provided", () => { + sendPortMessage(buttonMessageConnectorSpy, { + command: "redirectAutofillInlineMenuFocusOut", + direction: RedirectFocusDirection.Next, + portKey, }); - it("will set the overlayVisibility property", async () => { - sendMockExtensionMessage({ command: "getAutofillOverlayVisibility" }); - await flushPromises(); - - expect(await overlayBackground["getOverlayVisibility"]()).toBe( - AutofillOverlayVisibility.OnFieldFocus, - ); - }); - - it("returns the overlayVisibility property", async () => { - const sendMessageSpy = jest.fn(); - - sendMockExtensionMessage( - { command: "getAutofillOverlayVisibility" }, - undefined, - sendMessageSpy, - ); - await flushPromises(); - - expect(sendMessageSpy).toHaveBeenCalledWith(AutofillOverlayVisibility.OnFieldFocus); - }); + expect(tabSendMessageDataSpy).toHaveBeenCalledWith( + sender.tab, + "redirectAutofillInlineMenuFocusOut", + { direction: RedirectFocusDirection.Next }, + ); }); + }); - describe("checkAutofillOverlayFocused message handler", () => { - beforeEach(async () => { - await initOverlayElementPorts(); + describe("updateAutofillInlineMenuColorScheme message handler", () => { + it("sends a message to the button port to update the inline menu color scheme", async () => { + sendPortMessage(buttonMessageConnectorSpy, { + command: "updateAutofillInlineMenuColorScheme", + portKey, }); + await flushPromises(); - it("will check if the overlay list is focused if the list port is open", () => { - sendMockExtensionMessage({ command: "checkAutofillOverlayFocused" }); - - expect(listPortSpy.postMessage).toHaveBeenCalledWith({ - command: "checkAutofillOverlayListFocused", - }); - expect(buttonPortSpy.postMessage).not.toHaveBeenCalledWith({ - command: "checkAutofillOverlayButtonFocused", - }); - }); - - it("will check if the overlay button is focused if the list port is not open", () => { - overlayBackground["overlayListPort"] = undefined; - - sendMockExtensionMessage({ command: "checkAutofillOverlayFocused" }); - - expect(buttonPortSpy.postMessage).toHaveBeenCalledWith({ - command: "checkAutofillOverlayButtonFocused", - }); - expect(listPortSpy.postMessage).not.toHaveBeenCalledWith({ - command: "checkAutofillOverlayListFocused", - }); - }); - }); - - describe("focusAutofillOverlayList message handler", () => { - it("will send a `focusOverlayList` message to the overlay list port", async () => { - await initOverlayElementPorts({ initList: true, initButton: false }); - - sendMockExtensionMessage({ command: "focusAutofillOverlayList" }); - - expect(listPortSpy.postMessage).toHaveBeenCalledWith({ command: "focusOverlayList" }); - }); - }); - - describe("updateAutofillOverlayPosition message handler", () => { - beforeEach(async () => { - await overlayBackground["handlePortOnConnect"]( - createPortSpyMock(AutofillOverlayPort.List), - ); - listPortSpy = overlayBackground["overlayListPort"]; - - await overlayBackground["handlePortOnConnect"]( - createPortSpyMock(AutofillOverlayPort.Button), - ); - buttonPortSpy = overlayBackground["overlayButtonPort"]; - }); - - it("ignores updating the position if the overlay element type is not provided", () => { - sendMockExtensionMessage({ command: "updateAutofillOverlayPosition" }); - - expect(listPortSpy.postMessage).not.toHaveBeenCalledWith({ - command: "updateIframePosition", - styles: expect.anything(), - }); - expect(buttonPortSpy.postMessage).not.toHaveBeenCalledWith({ - command: "updateIframePosition", - styles: expect.anything(), - }); - }); - - it("skips updating the position if the most recently focused field is different than the message sender", () => { - const sender = mock({ tab: { id: 1 } }); - const focusedFieldData = createFocusedFieldDataMock({ tabId: 2 }); - sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); - - sendMockExtensionMessage({ command: "updateAutofillOverlayPosition" }, sender); - - expect(listPortSpy.postMessage).not.toHaveBeenCalledWith({ - command: "updateIframePosition", - styles: expect.anything(), - }); - expect(buttonPortSpy.postMessage).not.toHaveBeenCalledWith({ - command: "updateIframePosition", - styles: expect.anything(), - }); - }); - - it("updates the overlay button's position", () => { - const focusedFieldData = createFocusedFieldDataMock(); - sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); - - sendMockExtensionMessage({ - command: "updateAutofillOverlayPosition", - overlayElement: AutofillOverlayElement.Button, - }); - - expect(buttonPortSpy.postMessage).toHaveBeenCalledWith({ - command: "updateIframePosition", - styles: { height: "2px", left: "4px", top: "2px", width: "2px" }, - }); - }); - - it("modifies the overlay button's height for medium sized input elements", () => { - const focusedFieldData = createFocusedFieldDataMock({ - focusedFieldRects: { top: 1, left: 2, height: 35, width: 4 }, - }); - sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); - - sendMockExtensionMessage({ - command: "updateAutofillOverlayPosition", - overlayElement: AutofillOverlayElement.Button, - }); - - expect(buttonPortSpy.postMessage).toHaveBeenCalledWith({ - command: "updateIframePosition", - styles: { height: "20px", left: "-22px", top: "8px", width: "20px" }, - }); - }); - - it("modifies the overlay button's height for large sized input elements", () => { - const focusedFieldData = createFocusedFieldDataMock({ - focusedFieldRects: { top: 1, left: 2, height: 50, width: 4 }, - }); - sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); - - sendMockExtensionMessage({ - command: "updateAutofillOverlayPosition", - overlayElement: AutofillOverlayElement.Button, - }); - - expect(buttonPortSpy.postMessage).toHaveBeenCalledWith({ - command: "updateIframePosition", - styles: { height: "27px", left: "-32px", top: "13px", width: "27px" }, - }); - }); - - it("takes into account the right padding of the focused field in positioning the button if the right padding of the field is larger than the left padding", () => { - const focusedFieldData = createFocusedFieldDataMock({ - focusedFieldStyles: { paddingRight: "20px", paddingLeft: "6px" }, - }); - sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); - - sendMockExtensionMessage({ - command: "updateAutofillOverlayPosition", - overlayElement: AutofillOverlayElement.Button, - }); - - expect(buttonPortSpy.postMessage).toHaveBeenCalledWith({ - command: "updateIframePosition", - styles: { height: "2px", left: "-18px", top: "2px", width: "2px" }, - }); - }); - - it("will post a message to the overlay list facilitating an update of the list's position", () => { - const sender = mock({ tab: { id: 1 } }); - const focusedFieldData = createFocusedFieldDataMock(); - sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); - - overlayBackground["updateOverlayPosition"]( - { overlayElement: AutofillOverlayElement.List }, - sender, - ); - sendMockExtensionMessage({ - command: "updateAutofillOverlayPosition", - overlayElement: AutofillOverlayElement.List, - }); - - expect(listPortSpy.postMessage).toHaveBeenCalledWith({ - command: "updateIframePosition", - styles: { left: "2px", top: "4px", width: "4px" }, - }); - }); - }); - - describe("updateOverlayHidden", () => { - beforeEach(async () => { - await initOverlayElementPorts(); - }); - - it("returns early if the display value is not provided", () => { - const message = { - command: "updateAutofillOverlayHidden", - }; - - sendMockExtensionMessage(message); - - expect(buttonPortSpy.postMessage).not.toHaveBeenCalledWith(message); - expect(listPortSpy.postMessage).not.toHaveBeenCalledWith(message); - }); - - it("posts a message to the overlay button and list with the display value", () => { - const message = { command: "updateAutofillOverlayHidden", display: "none" }; - - sendMockExtensionMessage(message); - - expect(overlayBackground["overlayButtonPort"].postMessage).toHaveBeenCalledWith({ - command: "updateOverlayHidden", - styles: { - display: message.display, - }, - }); - expect(overlayBackground["overlayListPort"].postMessage).toHaveBeenCalledWith({ - command: "updateOverlayHidden", - styles: { - display: message.display, - }, - }); - }); - }); - - describe("collectPageDetailsResponse message handler", () => { - let sender: chrome.runtime.MessageSender; - const pageDetails1 = createAutofillPageDetailsMock({ - login: { username: "username1", password: "password1" }, - }); - const pageDetails2 = createAutofillPageDetailsMock({ - login: { username: "username2", password: "password2" }, - }); - - beforeEach(() => { - sender = mock({ tab: { id: 1 } }); - }); - - it("stores the page details provided by the message by the tab id of the sender", () => { - sendMockExtensionMessage( - { command: "collectPageDetailsResponse", details: pageDetails1 }, - sender, - ); - - expect(overlayBackground["pageDetailsForTab"][sender.tab.id]).toStrictEqual( - new Map([ - [sender.frameId, { frameId: sender.frameId, tab: sender.tab, details: pageDetails1 }], - ]), - ); - }); - - it("updates the page details for a tab that already has a set of page details stored ", () => { - const secondFrameSender = mock({ - tab: { id: 1 }, - frameId: 3, - }); - overlayBackground["pageDetailsForTab"][sender.tab.id] = new Map([ - [sender.frameId, { frameId: sender.frameId, tab: sender.tab, details: pageDetails1 }], - ]); - - sendMockExtensionMessage( - { command: "collectPageDetailsResponse", details: pageDetails2 }, - secondFrameSender, - ); - - expect(overlayBackground["pageDetailsForTab"][sender.tab.id]).toStrictEqual( - new Map([ - [sender.frameId, { frameId: sender.frameId, tab: sender.tab, details: pageDetails1 }], - [ - secondFrameSender.frameId, - { - frameId: secondFrameSender.frameId, - tab: secondFrameSender.tab, - details: pageDetails2, - }, - ], - ]), - ); - }); - }); - - describe("unlockCompleted message handler", () => { - let getAuthStatusSpy: jest.SpyInstance; - - beforeEach(() => { - overlayBackground["userAuthStatus"] = AuthenticationStatus.LoggedOut; - jest.spyOn(BrowserApi, "tabSendMessageData"); - getAuthStatusSpy = jest - .spyOn(overlayBackground as any, "getAuthStatus") - .mockImplementation(() => { - overlayBackground["userAuthStatus"] = AuthenticationStatus.Unlocked; - return Promise.resolve(AuthenticationStatus.Unlocked); - }); - }); - - it("updates the user's auth status but does not open the overlay", async () => { - const message = { - command: "unlockCompleted", - data: { - commandToRetry: { message: { command: "" } }, - }, - }; - - sendMockExtensionMessage(message); - await flushPromises(); - - expect(getAuthStatusSpy).toHaveBeenCalled(); - expect(BrowserApi.tabSendMessageData).not.toHaveBeenCalled(); - }); - - it("updates user's auth status and opens the overlay if a follow up command is provided", async () => { - const sender = mock({ tab: { id: 1 } }); - const message = { - command: "unlockCompleted", - data: { - commandToRetry: { message: { command: "openAutofillOverlay" } }, - }, - }; - jest.spyOn(BrowserApi, "getTabFromCurrentWindowId").mockResolvedValueOnce(sender.tab); - - sendMockExtensionMessage(message); - await flushPromises(); - - expect(getAuthStatusSpy).toHaveBeenCalled(); - expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith( - sender.tab, - "openAutofillOverlay", - { - isFocusingFieldElement: true, - isOpeningFullOverlay: false, - authStatus: AuthenticationStatus.Unlocked, - }, - ); - }); - }); - - describe("extension messages that trigger an update of the inline menu ciphers", () => { - const extensionMessages = [ - "addedCipher", - "addEditCipherSubmitted", - "editedCipher", - "deletedCipher", - ]; - - beforeEach(() => { - jest.spyOn(overlayBackground, "updateOverlayCiphers").mockImplementation(); - }); - - extensionMessages.forEach((message) => { - it(`triggers an update of the overlay ciphers when the ${message} message is received`, () => { - sendMockExtensionMessage({ command: message }); - expect(overlayBackground.updateOverlayCiphers).toHaveBeenCalled(); - }); + expect(buttonPortSpy.postMessage).toHaveBeenCalledWith({ + command: "updateAutofillInlineMenuColorScheme", }); }); }); }); - describe("handlePortOnConnect", () => { - beforeEach(() => { - jest.spyOn(overlayBackground as any, "updateOverlayPosition").mockImplementation(); - jest.spyOn(overlayBackground as any, "getAuthStatus").mockImplementation(); - jest.spyOn(overlayBackground as any, "getTranslations").mockImplementation(); - jest.spyOn(overlayBackground as any, "getOverlayCipherData").mockImplementation(); + describe("inline menu list message handlers", () => { + let sender: chrome.runtime.MessageSender; + const portKey = "inlineMenuListPort"; + + beforeEach(async () => { + sender = mock({ tab: { id: 1 } }); + portKeyForTabSpy[sender.tab.id] = portKey; + activeAccountStatusMock$.next(AuthenticationStatus.Unlocked); + await initOverlayElementPorts(); + listMessageConnectorSpy.sender = sender; + openUnlockPopoutSpy.mockImplementation(); }); + describe("checkAutofillInlineMenuButtonFocused message handler", () => { + it("sends a message to the inline menu button to check if the element is focused", async () => { + sendPortMessage(listMessageConnectorSpy, { + command: "checkAutofillInlineMenuButtonFocused", + portKey, + }); + + expect(buttonPortSpy.postMessage).toHaveBeenCalledWith({ + command: "checkAutofillInlineMenuButtonFocused", + }); + }); + }); + + describe("autofillInlineMenuBlurred message handler", () => { + it("sends a message to the inline menu button to check if the element is focused", async () => { + sendPortMessage(listMessageConnectorSpy, { + command: "autofillInlineMenuBlurred", + portKey, + }); + + expect(buttonPortSpy.postMessage).toHaveBeenCalledWith({ + command: "checkAutofillInlineMenuButtonFocused", + }); + }); + }); + + describe("unlockVault message handler", () => { + it("opens the unlock vault popout", async () => { + activeAccountStatusMock$.next(AuthenticationStatus.Locked); + tabsSendMessageSpy.mockImplementation(); + + sendPortMessage(listMessageConnectorSpy, { command: "unlockVault", portKey }); + await flushPromises(); + + expect(openUnlockPopoutSpy).toHaveBeenCalled(); + }); + }); + + describe("fillAutofillInlineMenuCipher message handler", () => { + const pageDetails = createAutofillPageDetailsMock({ + login: { username: "username1", password: "password1" }, + }); + + it("ignores the fill request if the overlay cipher id is not provided", async () => { + sendPortMessage(listMessageConnectorSpy, { + command: "fillAutofillInlineMenuCipher", + portKey, + }); + await flushPromises(); + + expect(autofillService.isPasswordRepromptRequired).not.toHaveBeenCalled(); + expect(autofillService.doAutoFill).not.toHaveBeenCalled(); + }); + + it("ignores the fill request if the tab does not contain any identified page details", async () => { + sendPortMessage(listMessageConnectorSpy, { + command: "fillAutofillInlineMenuCipher", + inlineMenuCipherId: "inline-menu-cipher-1", + portKey, + }); + await flushPromises(); + + expect(autofillService.isPasswordRepromptRequired).not.toHaveBeenCalled(); + expect(autofillService.doAutoFill).not.toHaveBeenCalled(); + }); + + it("ignores the fill request if a master password reprompt is required", async () => { + const cipher = mock({ + reprompt: CipherRepromptType.Password, + type: CipherType.Login, + }); + overlayBackground["inlineMenuCiphers"] = new Map([["inline-menu-cipher-1", cipher]]); + overlayBackground["pageDetailsForTab"][sender.tab.id] = new Map([ + [sender.frameId, { frameId: sender.frameId, tab: sender.tab, details: pageDetails }], + ]); + autofillService.isPasswordRepromptRequired.mockResolvedValue(true); + + sendPortMessage(listMessageConnectorSpy, { + command: "fillAutofillInlineMenuCipher", + inlineMenuCipherId: "inline-menu-cipher-1", + portKey, + }); + await flushPromises(); + + expect(autofillService.isPasswordRepromptRequired).toHaveBeenCalledWith(cipher, sender.tab); + expect(autofillService.doAutoFill).not.toHaveBeenCalled(); + }); + + it("auto-fills the selected cipher and move it to the top of the front of the ciphers map", async () => { + const cipher1 = mock({ id: "inline-menu-cipher-1" }); + const cipher2 = mock({ id: "inline-menu-cipher-2" }); + const cipher3 = mock({ id: "inline-menu-cipher-3" }); + overlayBackground["inlineMenuCiphers"] = new Map([ + ["inline-menu-cipher-1", cipher1], + ["inline-menu-cipher-2", cipher2], + ["inline-menu-cipher-3", cipher3], + ]); + const pageDetailsForTab = { + frameId: sender.frameId, + tab: sender.tab, + details: pageDetails, + }; + overlayBackground["pageDetailsForTab"][sender.tab.id] = new Map([ + [sender.frameId, pageDetailsForTab], + ]); + autofillService.isPasswordRepromptRequired.mockResolvedValue(false); + + sendPortMessage(listMessageConnectorSpy, { + command: "fillAutofillInlineMenuCipher", + inlineMenuCipherId: "inline-menu-cipher-2", + portKey, + }); + await flushPromises(); + + expect(autofillService.isPasswordRepromptRequired).toHaveBeenCalledWith( + cipher2, + sender.tab, + ); + expect(autofillService.doAutoFill).toHaveBeenCalledWith({ + tab: sender.tab, + cipher: cipher2, + pageDetails: [pageDetailsForTab], + fillNewPassword: true, + allowTotpAutofill: true, + }); + expect(overlayBackground["inlineMenuCiphers"].entries()).toStrictEqual( + new Map([ + ["inline-menu-cipher-2", cipher2], + ["inline-menu-cipher-1", cipher1], + ["inline-menu-cipher-3", cipher3], + ]).entries(), + ); + }); + + it("copies the cipher's totp code to the clipboard after filling", async () => { + const cipher1 = mock({ id: "inline-menu-cipher-1" }); + overlayBackground["inlineMenuCiphers"] = new Map([["inline-menu-cipher-1", cipher1]]); + overlayBackground["pageDetailsForTab"][sender.tab.id] = new Map([ + [sender.frameId, { frameId: sender.frameId, tab: sender.tab, details: pageDetails }], + ]); + autofillService.isPasswordRepromptRequired.mockResolvedValue(false); + const copyToClipboardSpy = jest + .spyOn(overlayBackground["platformUtilsService"], "copyToClipboard") + .mockImplementation(); + autofillService.doAutoFill.mockResolvedValue("totp-code"); + + sendPortMessage(listMessageConnectorSpy, { + command: "fillAutofillInlineMenuCipher", + inlineMenuCipherId: "inline-menu-cipher-2", + portKey, + }); + await flushPromises(); + + expect(copyToClipboardSpy).toHaveBeenCalledWith("totp-code"); + }); + }); + + describe("addNewVaultItem message handler", () => { + it("skips sending the `addNewVaultItemFromOverlay` message if the sender tab does not contain the focused field", async () => { + const focusedFieldData = createFocusedFieldDataMock({ tabId: 2 }); + sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); + await flushPromises(); + + sendPortMessage(listMessageConnectorSpy, { command: "addNewVaultItem", portKey }); + await flushPromises(); + + expect(tabsSendMessageSpy).not.toHaveBeenCalled(); + }); + + it("sends a message to the tab to add a new vault item", async () => { + const focusedFieldData = createFocusedFieldDataMock(); + sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }, sender); + await flushPromises(); + + sendPortMessage(listMessageConnectorSpy, { command: "addNewVaultItem", portKey }); + await flushPromises(); + + expect(tabsSendMessageSpy).toHaveBeenCalledWith( + sender.tab, + { command: "addNewVaultItemFromOverlay" }, + { frameId: sender.frameId }, + ); + }); + }); + + describe("viewSelectedCipher message handler", () => { + let openViewVaultItemPopoutSpy: jest.SpyInstance; + + beforeEach(() => { + openViewVaultItemPopoutSpy = jest + .spyOn(overlayBackground as any, "openViewVaultItemPopout") + .mockImplementation(); + }); + + it("returns early if the passed cipher ID does not match one of the inline menu ciphers", async () => { + overlayBackground["inlineMenuCiphers"] = new Map([ + ["inline-menu-cipher-0", mock({ id: "inline-menu-cipher-0" })], + ]); + + sendPortMessage(listMessageConnectorSpy, { + command: "viewSelectedCipher", + inlineMenuCipherId: "inline-menu-cipher-1", + portKey, + }); + await flushPromises(); + + expect(openViewVaultItemPopoutSpy).not.toHaveBeenCalled(); + }); + + it("will open the view vault item popout with the selected cipher", async () => { + const cipher = mock({ id: "inline-menu-cipher-1" }); + overlayBackground["inlineMenuCiphers"] = new Map([ + ["inline-menu-cipher-0", mock({ id: "inline-menu-cipher-0" })], + ["inline-menu-cipher-1", cipher], + ]); + + sendPortMessage(listMessageConnectorSpy, { + command: "viewSelectedCipher", + inlineMenuCipherId: "inline-menu-cipher-1", + portKey, + }); + await flushPromises(); + + expect(openViewVaultItemPopoutSpy).toHaveBeenCalledWith(sender.tab, { + cipherId: cipher.id, + action: SHOW_AUTOFILL_BUTTON, + }); + }); + }); + + describe("redirectAutofillInlineMenuFocusOut message handler", () => { + it("redirects focus out of the inline menu list", async () => { + sendPortMessage(listMessageConnectorSpy, { + command: "redirectAutofillInlineMenuFocusOut", + direction: RedirectFocusDirection.Next, + portKey, + }); + await flushPromises(); + + expect(tabSendMessageDataSpy).toHaveBeenCalledWith( + sender.tab, + "redirectAutofillInlineMenuFocusOut", + { direction: RedirectFocusDirection.Next }, + ); + }); + }); + + describe("updateAutofillInlineMenuListHeight message handler", () => { + it("sends a message to the list port to update the menu iframe position", () => { + sendPortMessage(listMessageConnectorSpy, { + command: "updateAutofillInlineMenuListHeight", + styles: { height: "100px" }, + portKey, + }); + + expect(listPortSpy.postMessage).toHaveBeenCalledWith({ + command: "updateAutofillInlineMenuPosition", + styles: { height: "100px" }, + }); + }); + }); + }); + + describe("handle web navigation on committed events", () => { + describe("navigation event occurs in the top frame of the tab", () => { + it("removes the collected page details", async () => { + const sender = mock({ + tabId: 1, + frameId: 0, + }); + overlayBackground["pageDetailsForTab"][sender.tabId] = new Map([ + [sender.frameId, createPageDetailMock()], + ]); + + triggerWebNavigationOnCommittedEvent(sender); + await flushPromises(); + + expect(overlayBackground["pageDetailsForTab"][sender.tabId]).toBe(undefined); + }); + + it("clears the sub frames associated with the tab", () => { + const sender = mock({ + tabId: 1, + frameId: 0, + }); + const subFrameId = 10; + overlayBackground["subFrameOffsetsForTab"][sender.tabId] = new Map([ + [subFrameId, mock()], + ]); + + triggerWebNavigationOnCommittedEvent(sender); + + expect(overlayBackground["subFrameOffsetsForTab"][sender.tabId]).toBe(undefined); + }); + }); + + describe("navigation event occurs within sub frame", () => { + it("clears the sub frame offsets for the current frame", () => { + const sender = mock({ + tabId: 1, + frameId: 1, + }); + overlayBackground["subFrameOffsetsForTab"][sender.tabId] = new Map([ + [sender.frameId, mock()], + ]); + + triggerWebNavigationOnCommittedEvent(sender); + + expect(overlayBackground["subFrameOffsetsForTab"][sender.tabId].get(sender.frameId)).toBe( + undefined, + ); + }); + }); + }); + + describe("handle port onConnect", () => { it("skips setting up the overlay port if the port connection is not for an overlay element", async () => { const port = createPortSpyMock("not-an-overlay-element"); - await overlayBackground["handlePortOnConnect"](port); + triggerPortOnConnectEvent(port); + await flushPromises(); expect(port.onMessage.addListener).not.toHaveBeenCalled(); expect(port.postMessage).not.toHaveBeenCalled(); }); - it("sets up the overlay list port if the port connection is for the overlay list", async () => { - await initOverlayElementPorts({ initList: true, initButton: false }); + it("generates a random 12 character string used to validate port messages from the tab", async () => { + const port = createPortSpyMock(AutofillOverlayPort.Button); + overlayBackground["inlineMenuButtonPort"] = port; + + triggerPortOnConnectEvent(port); await flushPromises(); - expect(overlayBackground["overlayButtonPort"]).toBeUndefined(); - expect(listPortSpy.onMessage.addListener).toHaveBeenCalled(); - expect(listPortSpy.postMessage).toHaveBeenCalled(); - expect(overlayBackground["getAuthStatus"]).toHaveBeenCalled(); - expect(chrome.runtime.getURL).toHaveBeenCalledWith("overlay/list.css"); - expect(overlayBackground["getTranslations"]).toHaveBeenCalled(); - expect(overlayBackground["getOverlayCipherData"]).toHaveBeenCalled(); - expect(overlayBackground["updateOverlayPosition"]).toHaveBeenCalledWith( - { overlayElement: AutofillOverlayElement.List }, - listPortSpy.sender, - ); - }); - - it("sets up the overlay button port if the port connection is for the overlay button", async () => { - await initOverlayElementPorts({ initList: false, initButton: true }); - await flushPromises(); - - expect(overlayBackground["overlayListPort"]).toBeUndefined(); - expect(buttonPortSpy.onMessage.addListener).toHaveBeenCalled(); - expect(buttonPortSpy.postMessage).toHaveBeenCalled(); - expect(overlayBackground["getAuthStatus"]).toHaveBeenCalled(); - expect(chrome.runtime.getURL).toHaveBeenCalledWith("overlay/button.css"); - expect(overlayBackground["getTranslations"]).toHaveBeenCalled(); - expect(overlayBackground["updateOverlayPosition"]).toHaveBeenCalledWith( - { overlayElement: AutofillOverlayElement.Button }, - buttonPortSpy.sender, - ); + expect(portKeyForTabSpy[port.sender.tab.id]).toHaveLength(12); }); it("stores an existing overlay port so that it can be disconnected at a later time", async () => { - overlayBackground["overlayButtonPort"] = mock(); + overlayBackground["inlineMenuButtonPort"] = mock(); await initOverlayElementPorts({ initList: false, initButton: true }); await flushPromises(); expect(overlayBackground["expiredPorts"].length).toBe(1); }); + }); - it("gets the system theme", async () => { - themeStateService.selectedTheme$ = of(ThemeType.System); + describe("handle overlay element port onMessage", () => { + let sender: chrome.runtime.MessageSender; + const portKey = "inlineMenuListPort"; - await initOverlayElementPorts({ initList: true, initButton: false }); + beforeEach(async () => { + sender = mock({ tab: { id: 1 } }); + portKeyForTabSpy[sender.tab.id] = portKey; + activeAccountStatusMock$.next(AuthenticationStatus.Unlocked); + await initOverlayElementPorts(); + listMessageConnectorSpy.sender = sender; + openUnlockPopoutSpy.mockImplementation(); + }); + + it("ignores messages that do not contain a valid portKey", async () => { + triggerPortOnMessageEvent(buttonMessageConnectorSpy, { + command: "autofillInlineMenuBlurred", + }); await flushPromises(); - expect(listPortSpy.postMessage).toHaveBeenCalledWith( - expect.objectContaining({ theme: ThemeType.System }), - ); + expect(listPortSpy.postMessage).not.toHaveBeenCalledWith({ + command: "checkAutofillInlineMenuListFocused", + }); + }); + + it("ignores messages from ports that are not listened to", () => { + triggerPortOnMessageEvent(buttonPortSpy, { + command: "autofillInlineMenuBlurred", + portKey, + }); + + expect(listPortSpy.postMessage).not.toHaveBeenCalledWith({ + command: "checkAutofillInlineMenuListFocused", + }); }); }); - describe("handleOverlayElementPortMessage", () => { - beforeEach(async () => { + describe("handle port onDisconnect", () => { + it("sets the disconnected port to a `null` value", async () => { await initOverlayElementPorts(); - overlayBackground["userAuthStatus"] = AuthenticationStatus.Unlocked; - }); - it("ignores port messages that do not contain a handler", () => { - jest.spyOn(overlayBackground as any, "checkOverlayButtonFocused").mockImplementation(); + triggerPortOnDisconnectEvent(buttonPortSpy); + triggerPortOnDisconnectEvent(listPortSpy); + await flushPromises(); - sendPortMessage(buttonPortSpy, { command: "checkAutofillOverlayButtonFocused" }); - - expect(overlayBackground["checkOverlayButtonFocused"]).not.toHaveBeenCalled(); - }); - - describe("overlay button message handlers", () => { - it("unlocks the vault if the user auth status is not unlocked", () => { - overlayBackground["userAuthStatus"] = AuthenticationStatus.LoggedOut; - jest.spyOn(overlayBackground as any, "unlockVault").mockImplementation(); - - sendPortMessage(buttonPortSpy, { command: "overlayButtonClicked" }); - - expect(overlayBackground["unlockVault"]).toHaveBeenCalled(); - }); - - it("opens the autofill overlay if the auth status is unlocked", () => { - jest.spyOn(overlayBackground as any, "openOverlay").mockImplementation(); - - sendPortMessage(buttonPortSpy, { command: "overlayButtonClicked" }); - - expect(overlayBackground["openOverlay"]).toHaveBeenCalled(); - }); - - describe("closeAutofillOverlay", () => { - it("sends a `closeOverlay` message to the sender tab", () => { - jest.spyOn(BrowserApi, "tabSendMessageData"); - - sendPortMessage(buttonPortSpy, { command: "closeAutofillOverlay" }); - - expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith( - buttonPortSpy.sender.tab, - "closeAutofillOverlay", - { forceCloseOverlay: false }, - ); - }); - }); - - describe("forceCloseAutofillOverlay", () => { - it("sends a `closeOverlay` message to the sender tab with a `forceCloseOverlay` flag of `true` set", () => { - jest.spyOn(BrowserApi, "tabSendMessageData"); - - sendPortMessage(buttonPortSpy, { command: "forceCloseAutofillOverlay" }); - - expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith( - buttonPortSpy.sender.tab, - "closeAutofillOverlay", - { forceCloseOverlay: true }, - ); - }); - }); - - describe("overlayPageBlurred", () => { - it("checks if the overlay list is focused", () => { - jest.spyOn(overlayBackground as any, "checkOverlayListFocused"); - - sendPortMessage(buttonPortSpy, { command: "overlayPageBlurred" }); - - expect(overlayBackground["checkOverlayListFocused"]).toHaveBeenCalled(); - }); - }); - - describe("redirectOverlayFocusOut", () => { - beforeEach(() => { - jest.spyOn(BrowserApi, "tabSendMessageData"); - }); - - it("ignores the redirect message if the direction is not provided", () => { - sendPortMessage(buttonPortSpy, { command: "redirectOverlayFocusOut" }); - - expect(BrowserApi.tabSendMessageData).not.toHaveBeenCalled(); - }); - - it("sends the redirect message if the direction is provided", () => { - sendPortMessage(buttonPortSpy, { - command: "redirectOverlayFocusOut", - direction: RedirectFocusDirection.Next, - }); - - expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith( - buttonPortSpy.sender.tab, - "redirectOverlayFocusOut", - { direction: RedirectFocusDirection.Next }, - ); - }); - }); - }); - - describe("overlay list message handlers", () => { - describe("checkAutofillOverlayButtonFocused", () => { - it("checks on the focus state of the overlay button", () => { - jest.spyOn(overlayBackground as any, "checkOverlayButtonFocused").mockImplementation(); - - sendPortMessage(listPortSpy, { command: "checkAutofillOverlayButtonFocused" }); - - expect(overlayBackground["checkOverlayButtonFocused"]).toHaveBeenCalled(); - }); - }); - - describe("forceCloseAutofillOverlay", () => { - it("sends a `closeOverlay` message to the sender tab with a `forceCloseOverlay` flag of `true` set", () => { - jest.spyOn(BrowserApi, "tabSendMessageData"); - - sendPortMessage(listPortSpy, { command: "forceCloseAutofillOverlay" }); - - expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith( - listPortSpy.sender.tab, - "closeAutofillOverlay", - { forceCloseOverlay: true }, - ); - }); - }); - - describe("overlayPageBlurred", () => { - it("checks on the focus state of the overlay button", () => { - jest.spyOn(overlayBackground as any, "checkOverlayButtonFocused").mockImplementation(); - - sendPortMessage(listPortSpy, { command: "overlayPageBlurred" }); - - expect(overlayBackground["checkOverlayButtonFocused"]).toHaveBeenCalled(); - }); - }); - - describe("unlockVault", () => { - it("closes the autofill overlay and opens the unlock popout", async () => { - jest.spyOn(overlayBackground as any, "closeOverlay").mockImplementation(); - jest.spyOn(overlayBackground as any, "openUnlockPopout").mockImplementation(); - jest.spyOn(BrowserApi, "tabSendMessageData").mockImplementation(); - - sendPortMessage(listPortSpy, { command: "unlockVault" }); - await flushPromises(); - - expect(overlayBackground["closeOverlay"]).toHaveBeenCalledWith(listPortSpy); - expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith( - listPortSpy.sender.tab, - "addToLockedVaultPendingNotifications", - { - commandToRetry: { - message: { command: "openAutofillOverlay" }, - sender: listPortSpy.sender, - }, - target: "overlay.background", - }, - ); - expect(overlayBackground["openUnlockPopout"]).toHaveBeenCalledWith( - listPortSpy.sender.tab, - true, - ); - }); - }); - - describe("fillSelectedListItem", () => { - let getLoginCiphersSpy: jest.SpyInstance; - let isPasswordRepromptRequiredSpy: jest.SpyInstance; - let doAutoFillSpy: jest.SpyInstance; - let sender: chrome.runtime.MessageSender; - const pageDetails = createAutofillPageDetailsMock({ - login: { username: "username1", password: "password1" }, - }); - - beforeEach(() => { - getLoginCiphersSpy = jest.spyOn(overlayBackground["overlayLoginCiphers"], "get"); - isPasswordRepromptRequiredSpy = jest.spyOn( - overlayBackground["autofillService"], - "isPasswordRepromptRequired", - ); - doAutoFillSpy = jest.spyOn(overlayBackground["autofillService"], "doAutoFill"); - sender = mock({ tab: { id: 1 } }); - }); - - it("ignores the fill request if the overlay cipher id is not provided", async () => { - sendPortMessage(listPortSpy, { command: "fillSelectedListItem" }); - await flushPromises(); - - expect(getLoginCiphersSpy).not.toHaveBeenCalled(); - expect(isPasswordRepromptRequiredSpy).not.toHaveBeenCalled(); - expect(doAutoFillSpy).not.toHaveBeenCalled(); - }); - - it("ignores the fill request if the tab does not contain any identified page details", async () => { - sendPortMessage(listPortSpy, { - command: "fillSelectedListItem", - overlayCipherId: "overlay-cipher-1", - }); - await flushPromises(); - - expect(getLoginCiphersSpy).not.toHaveBeenCalled(); - expect(isPasswordRepromptRequiredSpy).not.toHaveBeenCalled(); - expect(doAutoFillSpy).not.toHaveBeenCalled(); - }); - - it("ignores the fill request if a master password reprompt is required", async () => { - const cipher = mock({ - reprompt: CipherRepromptType.Password, - type: CipherType.Login, - }); - overlayBackground["overlayLoginCiphers"] = new Map([["overlay-cipher-1", cipher]]); - overlayBackground["pageDetailsForTab"][sender.tab.id] = new Map([ - [sender.frameId, { frameId: sender.frameId, tab: sender.tab, details: pageDetails }], - ]); - getLoginCiphersSpy = jest.spyOn(overlayBackground["overlayLoginCiphers"], "get"); - isPasswordRepromptRequiredSpy.mockResolvedValue(true); - - sendPortMessage(listPortSpy, { - command: "fillSelectedListItem", - overlayCipherId: "overlay-cipher-1", - }); - await flushPromises(); - - expect(getLoginCiphersSpy).toHaveBeenCalled(); - expect(isPasswordRepromptRequiredSpy).toHaveBeenCalledWith( - cipher, - listPortSpy.sender.tab, - ); - expect(doAutoFillSpy).not.toHaveBeenCalled(); - }); - - it("auto-fills the selected cipher and move it to the top of the front of the ciphers map", async () => { - const cipher1 = mock({ id: "overlay-cipher-1" }); - const cipher2 = mock({ id: "overlay-cipher-2" }); - const cipher3 = mock({ id: "overlay-cipher-3" }); - overlayBackground["overlayLoginCiphers"] = new Map([ - ["overlay-cipher-1", cipher1], - ["overlay-cipher-2", cipher2], - ["overlay-cipher-3", cipher3], - ]); - const pageDetailsForTab = { - frameId: sender.frameId, - tab: sender.tab, - details: pageDetails, - }; - overlayBackground["pageDetailsForTab"][sender.tab.id] = new Map([ - [sender.frameId, pageDetailsForTab], - ]); - isPasswordRepromptRequiredSpy.mockResolvedValue(false); - - sendPortMessage(listPortSpy, { - command: "fillSelectedListItem", - overlayCipherId: "overlay-cipher-2", - }); - await flushPromises(); - - expect(isPasswordRepromptRequiredSpy).toHaveBeenCalledWith( - cipher2, - listPortSpy.sender.tab, - ); - expect(doAutoFillSpy).toHaveBeenCalledWith({ - tab: listPortSpy.sender.tab, - cipher: cipher2, - pageDetails: [pageDetailsForTab], - fillNewPassword: true, - allowTotpAutofill: true, - }); - expect(overlayBackground["overlayLoginCiphers"].entries()).toStrictEqual( - new Map([ - ["overlay-cipher-2", cipher2], - ["overlay-cipher-1", cipher1], - ["overlay-cipher-3", cipher3], - ]).entries(), - ); - }); - - it("copies the cipher's totp code to the clipboard after filling", async () => { - const cipher1 = mock({ id: "overlay-cipher-1" }); - overlayBackground["overlayLoginCiphers"] = new Map([["overlay-cipher-1", cipher1]]); - overlayBackground["pageDetailsForTab"][sender.tab.id] = new Map([ - [sender.frameId, { frameId: sender.frameId, tab: sender.tab, details: pageDetails }], - ]); - isPasswordRepromptRequiredSpy.mockResolvedValue(false); - const copyToClipboardSpy = jest - .spyOn(overlayBackground["platformUtilsService"], "copyToClipboard") - .mockImplementation(); - doAutoFillSpy.mockReturnValueOnce("totp-code"); - - sendPortMessage(listPortSpy, { - command: "fillSelectedListItem", - overlayCipherId: "overlay-cipher-2", - }); - await flushPromises(); - - expect(copyToClipboardSpy).toHaveBeenCalledWith("totp-code"); - }); - }); - - describe("getNewVaultItemDetails", () => { - it("will send an addNewVaultItemFromOverlay message", async () => { - jest.spyOn(BrowserApi, "tabSendMessage"); - - sendPortMessage(listPortSpy, { command: "addNewVaultItem" }); - await flushPromises(); - - expect(BrowserApi.tabSendMessage).toHaveBeenCalledWith(listPortSpy.sender.tab, { - command: "addNewVaultItemFromOverlay", - }); - }); - }); - - describe("viewSelectedCipher", () => { - let openViewVaultItemPopoutSpy: jest.SpyInstance; - - beforeEach(() => { - openViewVaultItemPopoutSpy = jest - .spyOn(overlayBackground as any, "openViewVaultItemPopout") - .mockImplementation(); - }); - - it("returns early if the passed cipher ID does not match one of the overlay login ciphers", async () => { - overlayBackground["overlayLoginCiphers"] = new Map([ - ["overlay-cipher-0", mock({ id: "overlay-cipher-0" })], - ]); - - sendPortMessage(listPortSpy, { - command: "viewSelectedCipher", - overlayCipherId: "overlay-cipher-1", - }); - await flushPromises(); - - expect(openViewVaultItemPopoutSpy).not.toHaveBeenCalled(); - }); - - it("will open the view vault item popout with the selected cipher", async () => { - const cipher = mock({ id: "overlay-cipher-1" }); - overlayBackground["overlayLoginCiphers"] = new Map([ - ["overlay-cipher-0", mock({ id: "overlay-cipher-0" })], - ["overlay-cipher-1", cipher], - ]); - - sendPortMessage(listPortSpy, { - command: "viewSelectedCipher", - overlayCipherId: "overlay-cipher-1", - }); - await flushPromises(); - - expect(overlayBackground["openViewVaultItemPopout"]).toHaveBeenCalledWith( - listPortSpy.sender.tab, - { - cipherId: cipher.id, - action: SHOW_AUTOFILL_BUTTON, - }, - ); - }); - }); - - describe("redirectOverlayFocusOut", () => { - it("redirects focus out of the overlay list", async () => { - const message = { - command: "redirectOverlayFocusOut", - direction: RedirectFocusDirection.Next, - }; - const redirectOverlayFocusOutSpy = jest.spyOn( - overlayBackground as any, - "redirectOverlayFocusOut", - ); - - sendPortMessage(listPortSpy, message); - await flushPromises(); - - expect(redirectOverlayFocusOutSpy).toHaveBeenCalledWith(message, listPortSpy); - }); - }); + expect(overlayBackground["inlineMenuListPort"]).toBeNull(); + expect(overlayBackground["inlineMenuButtonPort"]).toBeNull(); }); }); }); diff --git a/apps/browser/src/autofill/background/overlay.background.ts b/apps/browser/src/autofill/background/overlay.background.ts index 2f80790134e..3b770af2004 100644 --- a/apps/browser/src/autofill/background/overlay.background.ts +++ b/apps/browser/src/autofill/background/overlay.background.ts @@ -1,13 +1,18 @@ -import { firstValueFrom } from "rxjs"; +import { firstValueFrom, merge, Subject, throttleTime } from "rxjs"; +import { debounceTime, switchMap } from "rxjs/operators"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; -import { SHOW_AUTOFILL_BUTTON } from "@bitwarden/common/autofill/constants"; +import { + AutofillOverlayVisibility, + SHOW_AUTOFILL_BUTTON, +} from "@bitwarden/common/autofill/constants"; import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; import { InlineMenuVisibilitySetting } from "@bitwarden/common/autofill/types"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; @@ -21,80 +26,118 @@ import { LoginView } from "@bitwarden/common/vault/models/view/login.view"; import { openUnlockPopout } from "../../auth/popup/utils/auth-popout-window"; import { BrowserApi } from "../../platform/browser/browser-api"; import { - openViewVaultItemPopout, openAddEditVaultItemPopout, + openViewVaultItemPopout, } from "../../vault/popup/utils/vault-popout-window"; -import { AutofillService, PageDetail } from "../services/abstractions/autofill.service"; -import { AutofillOverlayElement, AutofillOverlayPort } from "../utils/autofill-overlay.enum"; +import { + AutofillOverlayElement, + AutofillOverlayPort, + MAX_SUB_FRAME_DEPTH, +} from "../enums/autofill-overlay.enum"; +import { AutofillService } from "../services/abstractions/autofill.service"; +import { generateRandomChars } from "../utils"; import { LockedVaultPendingNotificationsData } from "./abstractions/notification.background"; import { FocusedFieldData, - OverlayBackgroundExtensionMessageHandlers, - OverlayButtonPortMessageHandlers, - OverlayCipherData, - OverlayListPortMessageHandlers, + OverlayAddNewItemMessage, OverlayBackground as OverlayBackgroundInterface, OverlayBackgroundExtensionMessage, - OverlayAddNewItemMessage, + OverlayBackgroundExtensionMessageHandlers, + InlineMenuButtonPortMessageHandlers, + InlineMenuCipherData, + InlineMenuListPortMessageHandlers, OverlayPortMessage, - WebsiteIconData, + PageDetailsForTab, + SubFrameOffsetData, + SubFrameOffsetsForTab, + CloseInlineMenuMessage, + ToggleInlineMenuHiddenMessage, } from "./abstractions/overlay.background"; -class OverlayBackground implements OverlayBackgroundInterface { +export class OverlayBackground implements OverlayBackgroundInterface { private readonly openUnlockPopout = openUnlockPopout; private readonly openViewVaultItemPopout = openViewVaultItemPopout; private readonly openAddEditVaultItemPopout = openAddEditVaultItemPopout; - private overlayLoginCiphers: Map = new Map(); - private pageDetailsForTab: Record< - chrome.runtime.MessageSender["tab"]["id"], - Map - > = {}; - private userAuthStatus: AuthenticationStatus = AuthenticationStatus.LoggedOut; - private overlayButtonPort: chrome.runtime.Port; - private overlayListPort: chrome.runtime.Port; + private pageDetailsForTab: PageDetailsForTab = {}; + private subFrameOffsetsForTab: SubFrameOffsetsForTab = {}; + private portKeyForTab: Record = {}; private expiredPorts: chrome.runtime.Port[] = []; + private inlineMenuButtonPort: chrome.runtime.Port; + private inlineMenuListPort: chrome.runtime.Port; + private inlineMenuCiphers: Map = new Map(); + private inlineMenuPageTranslations: Record; + private delayedCloseTimeout: number | NodeJS.Timeout; + private startInlineMenuFadeInSubject = new Subject(); + private cancelInlineMenuFadeInSubject = new Subject(); + private startUpdateInlineMenuPositionSubject = new Subject(); + private cancelUpdateInlineMenuPositionSubject = new Subject(); + private repositionInlineMenuSubject = new Subject(); + private rebuildSubFrameOffsetsSubject = new Subject(); private focusedFieldData: FocusedFieldData; - private overlayPageTranslations: Record; + private isFieldCurrentlyFocused: boolean = false; + private isFieldCurrentlyFilling: boolean = false; + private isInlineMenuButtonVisible: boolean = false; + private isInlineMenuListVisible: boolean = false; private iconsServerUrl: string; private readonly extensionMessageHandlers: OverlayBackgroundExtensionMessageHandlers = { - openAutofillOverlay: () => this.openOverlay(false), autofillOverlayElementClosed: ({ message, sender }) => this.overlayElementClosed(message, sender), autofillOverlayAddNewVaultItem: ({ message, sender }) => this.addNewVaultItem(message, sender), - getAutofillOverlayVisibility: () => this.getOverlayVisibility(), - checkAutofillOverlayFocused: () => this.checkOverlayFocused(), - focusAutofillOverlayList: () => this.focusOverlayList(), - updateAutofillOverlayPosition: ({ message, sender }) => - this.updateOverlayPosition(message, sender), - updateAutofillOverlayHidden: ({ message }) => this.updateOverlayHidden(message), + triggerAutofillOverlayReposition: ({ sender }) => this.triggerOverlayReposition(sender), + checkIsInlineMenuCiphersPopulated: ({ sender }) => + this.checkIsInlineMenuCiphersPopulated(sender), updateFocusedFieldData: ({ message, sender }) => this.setFocusedFieldData(message, sender), + updateIsFieldCurrentlyFocused: ({ message }) => this.updateIsFieldCurrentlyFocused(message), + checkIsFieldCurrentlyFocused: () => this.checkIsFieldCurrentlyFocused(), + updateIsFieldCurrentlyFilling: ({ message }) => this.updateIsFieldCurrentlyFilling(message), + checkIsFieldCurrentlyFilling: () => this.checkIsFieldCurrentlyFilling(), + getAutofillInlineMenuVisibility: () => this.getInlineMenuVisibility(), + openAutofillInlineMenu: () => this.openInlineMenu(false), + closeAutofillInlineMenu: ({ message, sender }) => this.closeInlineMenu(sender, message), + checkAutofillInlineMenuFocused: ({ sender }) => this.checkInlineMenuFocused(sender), + focusAutofillInlineMenuList: () => this.focusInlineMenuList(), + updateAutofillInlineMenuPosition: ({ message, sender }) => + this.updateInlineMenuPosition(message, sender), + updateAutofillInlineMenuElementIsVisibleStatus: ({ message, sender }) => + this.updateInlineMenuElementIsVisibleStatus(message, sender), + checkIsAutofillInlineMenuButtonVisible: () => this.checkIsInlineMenuButtonVisible(), + checkIsAutofillInlineMenuListVisible: () => this.checkIsInlineMenuListVisible(), + getCurrentTabFrameId: ({ sender }) => this.getSenderFrameId(sender), + updateSubFrameData: ({ message, sender }) => this.updateSubFrameData(message, sender), + triggerSubFrameFocusInRebuild: ({ sender }) => this.triggerSubFrameFocusInRebuild(sender), + destroyAutofillInlineMenuListeners: ({ message, sender }) => + this.triggerDestroyInlineMenuListeners(sender.tab, message.subFrameData.frameId), collectPageDetailsResponse: ({ message, sender }) => this.storePageDetails(message, sender), unlockCompleted: ({ message }) => this.unlockCompleted(message), + doFullSync: () => this.updateOverlayCiphers(), addedCipher: () => this.updateOverlayCiphers(), addEditCipherSubmitted: () => this.updateOverlayCiphers(), editedCipher: () => this.updateOverlayCiphers(), deletedCipher: () => this.updateOverlayCiphers(), }; - private readonly overlayButtonPortMessageHandlers: OverlayButtonPortMessageHandlers = { - overlayButtonClicked: ({ port }) => this.handleOverlayButtonClicked(port), - closeAutofillOverlay: ({ port }) => this.closeOverlay(port), - forceCloseAutofillOverlay: ({ port }) => this.closeOverlay(port, true), - overlayPageBlurred: () => this.checkOverlayListFocused(), - redirectOverlayFocusOut: ({ message, port }) => this.redirectOverlayFocusOut(message, port), + private readonly inlineMenuButtonPortMessageHandlers: InlineMenuButtonPortMessageHandlers = { + triggerDelayedAutofillInlineMenuClosure: ({ port }) => this.triggerDelayedInlineMenuClosure(), + autofillInlineMenuButtonClicked: ({ port }) => this.handleInlineMenuButtonClicked(port), + autofillInlineMenuBlurred: () => this.checkInlineMenuListFocused(), + redirectAutofillInlineMenuFocusOut: ({ message, port }) => + this.redirectInlineMenuFocusOut(message, port), + updateAutofillInlineMenuColorScheme: () => this.updateInlineMenuButtonColorScheme(), }; - private readonly overlayListPortMessageHandlers: OverlayListPortMessageHandlers = { - checkAutofillOverlayButtonFocused: () => this.checkOverlayButtonFocused(), - forceCloseAutofillOverlay: ({ port }) => this.closeOverlay(port, true), - overlayPageBlurred: () => this.checkOverlayButtonFocused(), + private readonly inlineMenuListPortMessageHandlers: InlineMenuListPortMessageHandlers = { + checkAutofillInlineMenuButtonFocused: () => this.checkInlineMenuButtonFocused(), + autofillInlineMenuBlurred: () => this.checkInlineMenuButtonFocused(), unlockVault: ({ port }) => this.unlockVault(port), - fillSelectedListItem: ({ message, port }) => this.fillSelectedOverlayListItem(message, port), + fillAutofillInlineMenuCipher: ({ message, port }) => this.fillInlineMenuCipher(message, port), addNewVaultItem: ({ port }) => this.getNewVaultItemDetails(port), viewSelectedCipher: ({ message, port }) => this.viewSelectedCipher(message, port), - redirectOverlayFocusOut: ({ message, port }) => this.redirectOverlayFocusOut(message, port), + redirectAutofillInlineMenuFocusOut: ({ message, port }) => + this.redirectInlineMenuFocusOut(message, port), + updateAutofillInlineMenuListHeight: ({ message }) => this.updateInlineMenuListHeight(message), }; constructor( + private logService: LogService, private cipherService: CipherService, private autofillService: AutofillService, private authService: AuthService, @@ -104,7 +147,53 @@ class OverlayBackground implements OverlayBackgroundInterface { private i18nService: I18nService, private platformUtilsService: PlatformUtilsService, private themeStateService: ThemeStateService, - ) {} + ) { + this.initOverlayEventObservables(); + } + + /** + * Sets up the extension message listeners and gets the settings for the + * overlay's visibility and the user's authentication status. + */ + async init() { + this.setupExtensionListeners(); + const env = await firstValueFrom(this.environmentService.environment$); + this.iconsServerUrl = env.getIconsUrl(); + } + + /** + * Initializes event observables that handle events which affect the overlay's behavior. + */ + private initOverlayEventObservables() { + this.repositionInlineMenuSubject + .pipe( + debounceTime(1000), + switchMap((sender) => this.repositionInlineMenu(sender)), + ) + .subscribe(); + this.rebuildSubFrameOffsetsSubject + .pipe( + throttleTime(100), + switchMap((sender) => this.rebuildSubFrameOffsets(sender)), + ) + .subscribe(); + + // Debounce used to update inline menu position + merge( + this.startUpdateInlineMenuPositionSubject.pipe(debounceTime(150)), + this.cancelUpdateInlineMenuPositionSubject, + ) + .pipe(switchMap((sender) => this.updateInlineMenuPositionAfterRepositionEvent(sender))) + .subscribe(); + + // FadeIn Observable behavior + merge( + this.startInlineMenuFadeInSubject.pipe(debounceTime(150)), + this.cancelInlineMenuFadeInSubject, + ) + .pipe(switchMap((cancelSignal) => this.triggerInlineMenuFadeIn(!!cancelSignal))) + .subscribe(); + } /** * Removes cached page details for a tab @@ -113,89 +202,83 @@ class OverlayBackground implements OverlayBackgroundInterface { * @param tabId - Used to reference the page details of a specific tab */ removePageDetails(tabId: number) { - if (!this.pageDetailsForTab[tabId]) { - return; + if (this.pageDetailsForTab[tabId]) { + this.pageDetailsForTab[tabId].clear(); + delete this.pageDetailsForTab[tabId]; } - this.pageDetailsForTab[tabId].clear(); - delete this.pageDetailsForTab[tabId]; + if (this.portKeyForTab[tabId]) { + delete this.portKeyForTab[tabId]; + } } /** - * Sets up the extension message listeners and gets the settings for the - * overlay's visibility and the user's authentication status. - */ - async init() { - this.setupExtensionMessageListeners(); - const env = await firstValueFrom(this.environmentService.environment$); - this.iconsServerUrl = env.getIconsUrl(); - await this.getOverlayVisibility(); - await this.getAuthStatus(); - } - - /** - * Updates the overlay list's ciphers and sends the updated list to the overlay list iframe. + * Updates the inline menu list's ciphers and sends the updated list to the inline menu list iframe. * Queries all ciphers for the given url, and sorts them by last used. Will not update the * list of ciphers if the extension is not unlocked. */ async updateOverlayCiphers() { const authStatus = await firstValueFrom(this.authService.activeAccountStatus$); if (authStatus !== AuthenticationStatus.Unlocked) { + if (this.focusedFieldData) { + void this.closeInlineMenuAfterCiphersUpdate(); + } return; } const currentTab = await BrowserApi.getTabFromCurrentWindowId(); - if (!currentTab?.url) { - return; + if (this.focusedFieldData && currentTab?.id !== this.focusedFieldData.tabId) { + void this.closeInlineMenuAfterCiphersUpdate(); } - this.overlayLoginCiphers = new Map(); - const ciphersViews = (await this.cipherService.getAllDecryptedForUrl(currentTab.url)).sort( - (a, b) => this.cipherService.sortCiphersByLastUsedThenName(a, b), - ); + this.inlineMenuCiphers = new Map(); + const ciphersViews = ( + await this.cipherService.getAllDecryptedForUrl(currentTab?.url || "") + ).sort((a, b) => this.cipherService.sortCiphersByLastUsedThenName(a, b)); for (let cipherIndex = 0; cipherIndex < ciphersViews.length; cipherIndex++) { - this.overlayLoginCiphers.set(`overlay-cipher-${cipherIndex}`, ciphersViews[cipherIndex]); + this.inlineMenuCiphers.set(`inline-menu-cipher-${cipherIndex}`, ciphersViews[cipherIndex]); } - const ciphers = await this.getOverlayCipherData(); - this.overlayListPort?.postMessage({ command: "updateOverlayListCiphers", ciphers }); - await BrowserApi.tabSendMessageData(currentTab, "updateIsOverlayCiphersPopulated", { - isOverlayCiphersPopulated: Boolean(ciphers.length), + const ciphers = await this.getInlineMenuCipherData(); + this.inlineMenuListPort?.postMessage({ + command: "updateAutofillInlineMenuListCiphers", + ciphers, }); } /** * Strips out unnecessary data from the ciphers and returns an array of - * objects that contain the cipher data needed for the overlay list. + * objects that contain the cipher data needed for the inline menu list. */ - private async getOverlayCipherData(): Promise { + private async getInlineMenuCipherData(): Promise { const showFavicons = await firstValueFrom(this.domainSettingsService.showFavicons$); - const overlayCiphersArray = Array.from(this.overlayLoginCiphers); - const overlayCipherData: OverlayCipherData[] = []; - let loginCipherIcon: WebsiteIconData; + const inlineMenuCiphersArray = Array.from(this.inlineMenuCiphers); + const inlineMenuCipherData: InlineMenuCipherData[] = []; - for (let cipherIndex = 0; cipherIndex < overlayCiphersArray.length; cipherIndex++) { - const [overlayCipherId, cipher] = overlayCiphersArray[cipherIndex]; - if (!loginCipherIcon && cipher.type === CipherType.Login) { - loginCipherIcon = buildCipherIcon(this.iconsServerUrl, cipher, showFavicons); - } + for (let cipherIndex = 0; cipherIndex < inlineMenuCiphersArray.length; cipherIndex++) { + const [inlineMenuCipherId, cipher] = inlineMenuCiphersArray[cipherIndex]; - overlayCipherData.push({ - id: overlayCipherId, + inlineMenuCipherData.push({ + id: inlineMenuCipherId, name: cipher.name, type: cipher.type, reprompt: cipher.reprompt, favorite: cipher.favorite, - icon: - cipher.type === CipherType.Login - ? loginCipherIcon - : buildCipherIcon(this.iconsServerUrl, cipher, showFavicons), + icon: buildCipherIcon(this.iconsServerUrl, cipher, showFavicons), login: cipher.type === CipherType.Login ? { username: cipher.login.username } : null, card: cipher.type === CipherType.Card ? cipher.card.subTitle : null, }); } - return overlayCipherData; + return inlineMenuCipherData; + } + + /** + * Gets the currently focused field and closes the inline menu on that tab. + */ + private async closeInlineMenuAfterCiphersUpdate() { + const focusedFieldTab = await BrowserApi.getTab(this.focusedFieldData.tabId); + this.closeInlineMenu({ tab: focusedFieldTab }, { forceCloseInlineMenu: true }); } /** @@ -215,6 +298,13 @@ class OverlayBackground implements OverlayBackgroundInterface { details: message.details, }; + if (pageDetails.frameId !== 0 && pageDetails.details.fields.length) { + void this.buildSubFrameOffsets(pageDetails.tab, pageDetails.frameId, pageDetails.details.url); + void BrowserApi.tabSendMessage(pageDetails.tab, { + command: "setupRebuildSubFrameOffsetsListeners", + }); + } + const pageDetailsMap = this.pageDetailsForTab[sender.tab.id]; if (!pageDetailsMap) { this.pageDetailsForTab[sender.tab.id] = new Map([[sender.frameId, pageDetails]]); @@ -225,22 +315,205 @@ class OverlayBackground implements OverlayBackgroundInterface { } /** - * Triggers autofill for the selected cipher in the overlay list. Also places - * the selected cipher at the top of the list of ciphers. + * Returns the frameId, called when calculating sub frame offsets within the tab. + * Is used to determine if we should reposition the inline menu when a resize event + * occurs within a frame. * - * @param overlayCipherId - Cipher ID corresponding to the overlayLoginCiphers map. Does not correspond to the actual cipher's ID. - * @param sender - The sender of the port message + * @param sender - The sender of the message */ - private async fillSelectedOverlayListItem( - { overlayCipherId }: OverlayPortMessage, - { sender }: chrome.runtime.Port, + private getSenderFrameId(sender: chrome.runtime.MessageSender) { + return sender.frameId; + } + + /** + * Handles sub frame offset calculations for the given tab and frame id. + * Is used in setting the position of the inline menu list and button. + * + * @param message - The message received from the `updateSubFrameData` command + * @param sender - The sender of the message + */ + private updateSubFrameData( + message: OverlayBackgroundExtensionMessage, + sender: chrome.runtime.MessageSender, ) { - const pageDetails = this.pageDetailsForTab[sender.tab.id]; - if (!overlayCipherId || !pageDetails?.size) { + const subFrameOffsetsForTab = this.subFrameOffsetsForTab[sender.tab.id]; + if (subFrameOffsetsForTab) { + subFrameOffsetsForTab.set(message.subFrameData.frameId, message.subFrameData); + } + } + + /** + * Builds the offset data for a sub frame of a tab. The offset data is used + * to calculate the position of the inline menu list and button. + * + * @param tab - The tab that the sub frame is associated with + * @param frameId - The frame ID of the sub frame + * @param url - The URL of the sub frame + * @param forceRebuild - Identifies whether the sub frame offsets should be rebuilt + */ + private async buildSubFrameOffsets( + tab: chrome.tabs.Tab, + frameId: number, + url: string, + forceRebuild: boolean = false, + ) { + let subFrameDepth = 0; + const tabId = tab.id; + let subFrameOffsetsForTab = this.subFrameOffsetsForTab[tabId]; + if (!subFrameOffsetsForTab) { + this.subFrameOffsetsForTab[tabId] = new Map(); + subFrameOffsetsForTab = this.subFrameOffsetsForTab[tabId]; + } + + if (!forceRebuild && subFrameOffsetsForTab.get(frameId)) { return; } - const cipher = this.overlayLoginCiphers.get(overlayCipherId); + const subFrameData: SubFrameOffsetData = { url, top: 0, left: 0, parentFrameIds: [0] }; + let frameDetails = await BrowserApi.getFrameDetails({ tabId, frameId }); + + while (frameDetails && frameDetails.parentFrameId > -1) { + subFrameDepth++; + if (subFrameDepth >= MAX_SUB_FRAME_DEPTH) { + subFrameOffsetsForTab.set(frameId, null); + this.triggerDestroyInlineMenuListeners(tab, frameId); + return; + } + + const subFrameOffset: SubFrameOffsetData = await BrowserApi.tabSendMessage( + tab, + { + command: "getSubFrameOffsets", + subFrameUrl: frameDetails.url, + subFrameId: frameDetails.documentId, + }, + { frameId: frameDetails.parentFrameId }, + ); + + if (!subFrameOffset) { + subFrameOffsetsForTab.set(frameId, null); + void BrowserApi.tabSendMessage( + tab, + { command: "getSubFrameOffsetsFromWindowMessage", subFrameId: frameId }, + { frameId }, + ); + return; + } + + subFrameData.top += subFrameOffset.top; + subFrameData.left += subFrameOffset.left; + if (!subFrameData.parentFrameIds.includes(frameDetails.parentFrameId)) { + subFrameData.parentFrameIds.push(frameDetails.parentFrameId); + } + + frameDetails = await BrowserApi.getFrameDetails({ + tabId, + frameId: frameDetails.parentFrameId, + }); + } + + subFrameOffsetsForTab.set(frameId, subFrameData); + } + + /** + * Triggers a removal and destruction of all + * + * @param tab - The tab that the sub frame is associated with + * @param frameId - The frame ID of the sub frame + */ + private triggerDestroyInlineMenuListeners(tab: chrome.tabs.Tab, frameId: number) { + this.logService.error( + "Excessive frame depth encountered, destroying inline menu on field within frame", + tab, + frameId, + ); + + void BrowserApi.tabSendMessage( + tab, + { command: "destroyAutofillInlineMenuListeners" }, + { frameId }, + ); + } + + /** + * Rebuilds the sub frame offsets for the tab associated with the sender. + * + * @param sender - The sender of the message + */ + private async rebuildSubFrameOffsets(sender: chrome.runtime.MessageSender) { + this.cancelUpdateInlineMenuPositionSubject.next(); + this.clearDelayedInlineMenuClosure(); + + const subFrameOffsetsForTab = this.subFrameOffsetsForTab[sender.tab.id]; + if (subFrameOffsetsForTab) { + const tabFrameIds = Array.from(subFrameOffsetsForTab.keys()); + for (const frameId of tabFrameIds) { + await this.buildSubFrameOffsets(sender.tab, frameId, sender.url, true); + } + } + } + + /** + * Handles updating the inline menu's position after rebuilding the sub frames + * for the provided tab. Will skip repositioning the inline menu if the field + * is not currently focused, or if the focused field has a value. + * + * @param sender - The sender of the message + */ + private async updateInlineMenuPositionAfterRepositionEvent( + sender: chrome.runtime.MessageSender | void, + ) { + if (!sender || !this.isFieldCurrentlyFocused) { + return; + } + + if (!this.checkIsInlineMenuButtonVisible()) { + void this.toggleInlineMenuHidden( + { isInlineMenuHidden: false, setTransparentInlineMenu: true }, + sender, + ); + } + + void this.updateInlineMenuPosition({ overlayElement: AutofillOverlayElement.Button }, sender); + + const mostRecentlyFocusedFieldHasValue = await BrowserApi.tabSendMessage( + sender.tab, + { command: "checkMostRecentlyFocusedFieldHasValue" }, + { frameId: this.focusedFieldData?.frameId }, + ); + + if ((await this.getInlineMenuVisibility()) === AutofillOverlayVisibility.OnButtonClick) { + return; + } + + if ( + mostRecentlyFocusedFieldHasValue && + (this.checkIsInlineMenuCiphersPopulated(sender) || + (await this.getAuthStatus()) !== AuthenticationStatus.Unlocked) + ) { + return; + } + + void this.updateInlineMenuPosition({ overlayElement: AutofillOverlayElement.List }, sender); + } + + /** + * Triggers autofill for the selected cipher in the inline menu list. Also places + * the selected cipher at the top of the list of ciphers. + * + * @param inlineMenuCipherId - Cipher ID corresponding to the inlineMenuCiphers map. Does not correspond to the actual cipher's ID. + * @param sender - The sender of the port message + */ + private async fillInlineMenuCipher( + { inlineMenuCipherId }: OverlayPortMessage, + { sender }: chrome.runtime.Port, + ) { + const pageDetails = this.pageDetailsForTab[sender.tab.id]; + if (!inlineMenuCipherId || !pageDetails?.size) { + return; + } + + const cipher = this.inlineMenuCiphers.get(inlineMenuCipherId); if (await this.autofillService.isPasswordRepromptRequired(cipher, sender.tab)) { return; @@ -257,47 +530,117 @@ class OverlayBackground implements OverlayBackgroundInterface { this.platformUtilsService.copyToClipboard(totpCode); } - this.overlayLoginCiphers = new Map([[overlayCipherId, cipher], ...this.overlayLoginCiphers]); + this.inlineMenuCiphers = new Map([[inlineMenuCipherId, cipher], ...this.inlineMenuCiphers]); } /** - * Checks if the overlay is focused. Will check the overlay list - * if it is open, otherwise it will check the overlay button. + * Checks if the inline menu is focused. Will check the inline menu list + * if it is open, otherwise it will check the inline menu button. */ - private checkOverlayFocused() { - if (this.overlayListPort) { - this.checkOverlayListFocused(); + private checkInlineMenuFocused(sender: chrome.runtime.MessageSender) { + if (!this.senderTabHasFocusedField(sender)) { + return; + } + + if (this.inlineMenuListPort) { + this.checkInlineMenuListFocused(); return; } - this.checkOverlayButtonFocused(); + this.checkInlineMenuButtonFocused(); } /** - * Posts a message to the overlay button iframe to check if it is focused. + * Posts a message to the inline menu button iframe to check if it is focused. */ - private checkOverlayButtonFocused() { - this.overlayButtonPort?.postMessage({ command: "checkAutofillOverlayButtonFocused" }); + private checkInlineMenuButtonFocused() { + this.inlineMenuButtonPort?.postMessage({ command: "checkAutofillInlineMenuButtonFocused" }); } /** - * Posts a message to the overlay list iframe to check if it is focused. + * Posts a message to the inline menu list iframe to check if it is focused. */ - private checkOverlayListFocused() { - this.overlayListPort?.postMessage({ command: "checkAutofillOverlayListFocused" }); + private checkInlineMenuListFocused() { + this.inlineMenuListPort?.postMessage({ command: "checkAutofillInlineMenuListFocused" }); } /** - * Sends a message to the sender tab to close the autofill overlay. + * Sends a message to the sender tab to close the autofill inline menu. * * @param sender - The sender of the port message - * @param forceCloseOverlay - Identifies whether the overlay should be force closed + * @param forceCloseInlineMenu - Identifies whether the inline menu should be forced closed + * @param overlayElement - The overlay element to close, either the list or button */ - private closeOverlay({ sender }: chrome.runtime.Port, forceCloseOverlay = false) { - // 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 - BrowserApi.tabSendMessageData(sender.tab, "closeAutofillOverlay", { forceCloseOverlay }); + private closeInlineMenu( + sender: chrome.runtime.MessageSender, + { forceCloseInlineMenu, overlayElement }: CloseInlineMenuMessage = {}, + ) { + const command = "closeAutofillInlineMenu"; + const sendOptions = { frameId: 0 }; + if (forceCloseInlineMenu) { + void BrowserApi.tabSendMessage(sender.tab, { command, overlayElement }, sendOptions); + this.isInlineMenuButtonVisible = false; + this.isInlineMenuListVisible = false; + return; + } + + if (this.isFieldCurrentlyFocused) { + return; + } + + if (this.isFieldCurrentlyFilling) { + void BrowserApi.tabSendMessage( + sender.tab, + { command, overlayElement: AutofillOverlayElement.List }, + sendOptions, + ); + this.isInlineMenuListVisible = false; + return; + } + + if (overlayElement === AutofillOverlayElement.Button) { + this.isInlineMenuButtonVisible = false; + } + + if (overlayElement === AutofillOverlayElement.List) { + this.isInlineMenuListVisible = false; + } + + if (!overlayElement) { + this.isInlineMenuButtonVisible = false; + this.isInlineMenuListVisible = false; + } + + void BrowserApi.tabSendMessage(sender.tab, { command, overlayElement }, sendOptions); + } + + /** + * Sends a message to the sender tab to trigger a delayed closure of the inline menu. + * This is used to ensure that we capture click events on the inline menu in the case + * that some on page programmatic method attempts to force focus redirection. + */ + private triggerDelayedInlineMenuClosure() { + if (this.isFieldCurrentlyFocused) { + return; + } + + this.clearDelayedInlineMenuClosure(); + this.delayedCloseTimeout = globalThis.setTimeout(() => { + const message = { command: "triggerDelayedAutofillInlineMenuClosure" }; + this.inlineMenuButtonPort?.postMessage(message); + this.inlineMenuListPort?.postMessage(message); + }, 100); + } + + /** + * Clears the delayed closure timeout for the inline menu, effectively + * cancelling the event from occurring. + */ + private clearDelayedInlineMenuClosure() { + if (this.delayedCloseTimeout) { + clearTimeout(this.delayedCloseTimeout); + } } /** @@ -311,61 +654,141 @@ class OverlayBackground implements OverlayBackgroundInterface { { overlayElement }: OverlayBackgroundExtensionMessage, sender: chrome.runtime.MessageSender, ) { - if (sender.tab.id !== this.focusedFieldData?.tabId) { + if (!this.senderTabHasFocusedField(sender)) { this.expiredPorts.forEach((port) => port.disconnect()); this.expiredPorts = []; + return; } if (overlayElement === AutofillOverlayElement.Button) { - this.overlayButtonPort?.disconnect(); - this.overlayButtonPort = null; + this.inlineMenuButtonPort?.disconnect(); + this.inlineMenuButtonPort = null; + this.isInlineMenuButtonVisible = false; return; } - this.overlayListPort?.disconnect(); - this.overlayListPort = null; + this.inlineMenuListPort?.disconnect(); + this.inlineMenuListPort = null; + this.isInlineMenuListVisible = false; } /** - * Updates the position of either the overlay list or button. The position + * Updates the position of either the inline menu list or button. The position * is based on the focused field's position and dimensions. * * @param overlayElement - The overlay element to update, either the list or button * @param sender - The sender of the port message */ - private updateOverlayPosition( + private async updateInlineMenuPosition( { overlayElement }: { overlayElement?: string }, sender: chrome.runtime.MessageSender, ) { - if (!overlayElement || sender.tab.id !== this.focusedFieldData?.tabId) { + if (!overlayElement || !this.senderTabHasFocusedField(sender)) { return; } + this.cancelInlineMenuFadeInAndPositionUpdate(); + + await BrowserApi.tabSendMessage( + sender.tab, + { command: "appendAutofillInlineMenuToDom", overlayElement }, + { frameId: 0 }, + ); + + const subFrameOffsetsForTab = this.subFrameOffsetsForTab[this.focusedFieldData.tabId]; + let subFrameOffsets: SubFrameOffsetData; + if (subFrameOffsetsForTab) { + subFrameOffsets = subFrameOffsetsForTab.get(this.focusedFieldData.frameId); + if (subFrameOffsets === null) { + this.rebuildSubFrameOffsetsSubject.next(sender); + this.startUpdateInlineMenuPositionSubject.next(sender); + return; + } + } + if (overlayElement === AutofillOverlayElement.Button) { - this.overlayButtonPort?.postMessage({ - command: "updateIframePosition", - styles: this.getOverlayButtonPosition(), + this.inlineMenuButtonPort?.postMessage({ + command: "updateAutofillInlineMenuPosition", + styles: this.getInlineMenuButtonPosition(subFrameOffsets), }); + this.startInlineMenuFadeIn(); return; } - this.overlayListPort?.postMessage({ - command: "updateIframePosition", - styles: this.getOverlayListPosition(), + this.inlineMenuListPort?.postMessage({ + command: "updateAutofillInlineMenuPosition", + styles: this.getInlineMenuListPosition(subFrameOffsets), }); + this.startInlineMenuFadeIn(); + } + + /** + * Triggers an update of the inline menu's visibility after the top level frame + * appends the element to the DOM. + * + * @param message - The message received from the content script + * @param sender - The sender of the port message + */ + private updateInlineMenuElementIsVisibleStatus( + message: OverlayBackgroundExtensionMessage, + sender: chrome.runtime.MessageSender, + ) { + if (!this.senderTabHasFocusedField(sender)) { + return; + } + + const { overlayElement, isVisible } = message; + if (overlayElement === AutofillOverlayElement.Button) { + this.isInlineMenuButtonVisible = isVisible; + return; + } + + if (overlayElement === AutofillOverlayElement.List) { + this.isInlineMenuListVisible = isVisible; + } + } + + /** + * Handles updating the opacity of both the inline menu button and list. + * This is used to simultaneously fade in the inline menu elements. + */ + private startInlineMenuFadeIn() { + this.cancelInlineMenuFadeIn(); + this.startInlineMenuFadeInSubject.next(); + } + + /** + * Clears the timeout used to fade in the inline menu elements. + */ + private cancelInlineMenuFadeIn() { + this.cancelInlineMenuFadeInSubject.next(true); + } + + /** + * Posts a message to the inline menu elements to trigger a fade in of the inline menu. + * + * @param cancelFadeIn - Signal passed to debounced observable to cancel the fade in + */ + private async triggerInlineMenuFadeIn(cancelFadeIn: boolean = false) { + if (cancelFadeIn) { + return; + } + + const message = { command: "fadeInAutofillInlineMenuIframe" }; + this.inlineMenuButtonPort?.postMessage(message); + this.inlineMenuListPort?.postMessage(message); } /** * Gets the position of the focused field and calculates the position - * of the overlay button based on the focused field's position and dimensions. + * of the inline menu button based on the focused field's position and dimensions. */ - private getOverlayButtonPosition() { - if (!this.focusedFieldData) { - return; - } + private getInlineMenuButtonPosition(subFrameOffsets: SubFrameOffsetData) { + const subFrameTopOffset = subFrameOffsets?.top || 0; + const subFrameLeftOffset = subFrameOffsets?.left || 0; const { top, left, width, height } = this.focusedFieldData.focusedFieldRects; const { paddingRight, paddingLeft } = this.focusedFieldData.focusedFieldStyles; @@ -374,15 +797,15 @@ class OverlayBackground implements OverlayBackgroundInterface { elementOffset = height >= 50 ? height * 0.47 : height * 0.42; } - const elementHeight = height - elementOffset; - const elementTopPosition = top + elementOffset / 2; - let elementLeftPosition = left + width - height + elementOffset / 2; - const fieldPaddingRight = parseInt(paddingRight, 10); const fieldPaddingLeft = parseInt(paddingLeft, 10); - if (fieldPaddingRight > fieldPaddingLeft) { - elementLeftPosition = left + width - height - (fieldPaddingRight - elementOffset + 2); - } + const elementHeight = height - elementOffset; + + const elementTopPosition = subFrameTopOffset + top + elementOffset / 2; + const elementLeftPosition = + fieldPaddingRight > fieldPaddingLeft + ? subFrameLeftOffset + left + width - height - (fieldPaddingRight - elementOffset + 2) + : subFrameLeftOffset + left + width - height + elementOffset / 2; return { top: `${Math.round(elementTopPosition)}px`, @@ -394,18 +817,17 @@ class OverlayBackground implements OverlayBackgroundInterface { /** * Gets the position of the focused field and calculates the position - * of the overlay list based on the focused field's position and dimensions. + * of the inline menu list based on the focused field's position and dimensions. */ - private getOverlayListPosition() { - if (!this.focusedFieldData) { - return; - } + private getInlineMenuListPosition(subFrameOffsets: SubFrameOffsetData) { + const subFrameTopOffset = subFrameOffsets?.top || 0; + const subFrameLeftOffset = subFrameOffsets?.left || 0; const { top, left, width, height } = this.focusedFieldData.focusedFieldRects; return { width: `${Math.round(width)}px`, - top: `${Math.round(top + height)}px`, - left: `${Math.round(left)}px`, + top: `${Math.round(top + height + subFrameTopOffset)}px`, + left: `${Math.round(left + subFrameLeftOffset)}px`, }; } @@ -419,109 +841,137 @@ class OverlayBackground implements OverlayBackgroundInterface { { focusedFieldData }: OverlayBackgroundExtensionMessage, sender: chrome.runtime.MessageSender, ) { - this.focusedFieldData = { ...focusedFieldData, tabId: sender.tab.id }; + if (this.focusedFieldData?.frameId && this.focusedFieldData.frameId !== sender.frameId) { + void BrowserApi.tabSendMessage( + sender.tab, + { command: "unsetMostRecentlyFocusedField" }, + { frameId: this.focusedFieldData.frameId }, + ); + } + + this.focusedFieldData = { ...focusedFieldData, tabId: sender.tab.id, frameId: sender.frameId }; } /** - * Updates the overlay's visibility based on the display property passed in the extension message. + * Updates the inline menu's visibility based on the display property passed in the extension message. * - * @param display - The display property of the overlay, either "block" or "none" + * @param display - The display property of the inline menu, either "block" or "none" + * @param sender - The sender of the extension message */ - private updateOverlayHidden({ display }: OverlayBackgroundExtensionMessage) { - if (!display) { + private async toggleInlineMenuHidden( + { isInlineMenuHidden, setTransparentInlineMenu }: ToggleInlineMenuHiddenMessage, + sender: chrome.runtime.MessageSender, + ) { + if (!this.senderTabHasFocusedField(sender)) { return; } - const portMessage = { command: "updateOverlayHidden", styles: { display } }; + this.cancelInlineMenuFadeIn(); + const display = isInlineMenuHidden ? "none" : "block"; + let styles: { display: string; opacity?: string } = { display }; - this.overlayButtonPort?.postMessage(portMessage); - this.overlayListPort?.postMessage(portMessage); + if (typeof setTransparentInlineMenu !== "undefined") { + const opacity = setTransparentInlineMenu ? "0" : "1"; + styles = { ...styles, opacity }; + } + + const portMessage = { command: "toggleAutofillInlineMenuHidden", styles }; + if (this.inlineMenuButtonPort) { + this.isInlineMenuButtonVisible = !isInlineMenuHidden; + this.inlineMenuButtonPort.postMessage(portMessage); + } + + if (this.inlineMenuListPort) { + this.isInlineMenuListVisible = !isInlineMenuHidden; + this.inlineMenuListPort.postMessage(portMessage); + } + + if (setTransparentInlineMenu) { + this.startInlineMenuFadeIn(); + } } /** - * Sends a message to the currently active tab to open the autofill overlay. + * Sends a message to the currently active tab to open the autofill inline menu. * - * @param isFocusingFieldElement - Identifies whether the field element should be focused when the overlay is opened - * @param isOpeningFullOverlay - Identifies whether the full overlay should be forced open regardless of other states + * @param isFocusingFieldElement - Identifies whether the field element should be focused when the inline menu is opened + * @param isOpeningFullInlineMenu - Identifies whether the full inline menu should be forced open regardless of other states */ - private async openOverlay(isFocusingFieldElement = false, isOpeningFullOverlay = false) { + private async openInlineMenu(isFocusingFieldElement = false, isOpeningFullInlineMenu = false) { + this.clearDelayedInlineMenuClosure(); const currentTab = await BrowserApi.getTabFromCurrentWindowId(); - await BrowserApi.tabSendMessageData(currentTab, "openAutofillOverlay", { - isFocusingFieldElement, - isOpeningFullOverlay, + await BrowserApi.tabSendMessage( + currentTab, + { + command: "openAutofillInlineMenu", + isFocusingFieldElement, + isOpeningFullInlineMenu, + authStatus: await this.getAuthStatus(), + }, + { + frameId: + this.focusedFieldData?.tabId === currentTab?.id ? this.focusedFieldData.frameId : 0, + }, + ); + } + + /** + * Gets the inline menu's visibility setting from the settings service. + */ + private async getInlineMenuVisibility(): Promise { + return await firstValueFrom(this.autofillSettingsService.inlineMenuVisibility$); + } + + /** + * Gets the user's authentication status from the auth service. If the user's authentication + * status has changed, the inline menu button's authentication status will be updated + * and the inline menu list's ciphers will be updated. + */ + private async getAuthStatus() { + return await firstValueFrom(this.authService.activeAccountStatus$); + } + + /** + * Sends a message to the inline menu button to update its authentication status. + */ + private async updateInlineMenuButtonAuthStatus() { + this.inlineMenuButtonPort?.postMessage({ + command: "updateInlineMenuButtonAuthStatus", authStatus: await this.getAuthStatus(), }); } /** - * Gets the overlay's visibility setting from the settings service. - */ - private async getOverlayVisibility(): Promise { - return await firstValueFrom(this.autofillSettingsService.inlineMenuVisibility$); - } - - /** - * Gets the user's authentication status from the auth service. If the user's - * authentication status has changed, the overlay button's authentication status - * will be updated and the overlay list's ciphers will be updated. - */ - private async getAuthStatus() { - const formerAuthStatus = this.userAuthStatus; - this.userAuthStatus = await this.authService.getAuthStatus(); - - if ( - this.userAuthStatus !== formerAuthStatus && - this.userAuthStatus === AuthenticationStatus.Unlocked - ) { - this.updateOverlayButtonAuthStatus(); - await this.updateOverlayCiphers(); - } - - return this.userAuthStatus; - } - - /** - * Sends a message to the overlay button to update its authentication status. - */ - private updateOverlayButtonAuthStatus() { - this.overlayButtonPort?.postMessage({ - command: "updateOverlayButtonAuthStatus", - authStatus: this.userAuthStatus, - }); - } - - /** - * Handles the overlay button being clicked. If the user is not authenticated, - * the vault will be unlocked. If the user is authenticated, the overlay will + * Handles the inline menu button being clicked. If the user is not authenticated, + * the vault will be unlocked. If the user is authenticated, the inline menu will * be opened. * - * @param port - The port of the overlay button + * @param port - The port of the inline menu button */ - private handleOverlayButtonClicked(port: chrome.runtime.Port) { - if (this.userAuthStatus !== AuthenticationStatus.Unlocked) { - // 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.unlockVault(port); + private async handleInlineMenuButtonClicked(port: chrome.runtime.Port) { + this.clearDelayedInlineMenuClosure(); + this.cancelInlineMenuFadeInAndPositionUpdate(); + + if ((await this.getAuthStatus()) !== AuthenticationStatus.Unlocked) { + await this.unlockVault(port); return; } - // 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.openOverlay(false, true); + await this.openInlineMenu(false, true); } /** * Facilitates opening the unlock popout window. * - * @param port - The port of the overlay list + * @param port - The port of the inline menu list */ private async unlockVault(port: chrome.runtime.Port) { const { sender } = port; - this.closeOverlay(port); + this.closeInlineMenu(port.sender); const retryMessage: LockedVaultPendingNotificationsData = { - commandToRetry: { message: { command: "openAutofillOverlay" }, sender }, + commandToRetry: { message: { command: "openAutofillInlineMenu" }, sender }, target: "overlay.background", }; await BrowserApi.tabSendMessageData( @@ -535,18 +985,19 @@ class OverlayBackground implements OverlayBackgroundInterface { /** * Triggers the opening of a vault item popout window associated * with the passed cipher ID. - * @param overlayCipherId - Cipher ID corresponding to the overlayLoginCiphers map. Does not correspond to the actual cipher's ID. + * @param inlineMenuCipherId - Cipher ID corresponding to the inlineMenuCiphers map. Does not correspond to the actual cipher's ID. * @param sender - The sender of the port message */ private async viewSelectedCipher( - { overlayCipherId }: OverlayPortMessage, + { inlineMenuCipherId }: OverlayPortMessage, { sender }: chrome.runtime.Port, ) { - const cipher = this.overlayLoginCiphers.get(overlayCipherId); + const cipher = this.inlineMenuCiphers.get(inlineMenuCipherId); if (!cipher) { return; } + this.closeInlineMenu(sender); await this.openViewVaultItemPopout(sender.tab, { cipherId: cipher.id, action: SHOW_AUTOFILL_BUTTON, @@ -554,32 +1005,33 @@ class OverlayBackground implements OverlayBackgroundInterface { } /** - * Facilitates redirecting focus to the overlay list. + * Facilitates redirecting focus to the inline menu list. */ - private focusOverlayList() { - this.overlayListPort?.postMessage({ command: "focusOverlayList" }); + private focusInlineMenuList() { + this.inlineMenuListPort?.postMessage({ command: "focusAutofillInlineMenuList" }); } /** - * Updates the authentication status for the user and opens the overlay if + * Updates the authentication status for the user and opens the inline menu if * a followup command is present in the message. * * @param message - Extension message received from the `unlockCompleted` command */ private async unlockCompleted(message: OverlayBackgroundExtensionMessage) { - await this.getAuthStatus(); + await this.updateInlineMenuButtonAuthStatus(); + await this.updateOverlayCiphers(); - if (message.data?.commandToRetry?.message?.command === "openAutofillOverlay") { - await this.openOverlay(true); + if (message.data?.commandToRetry?.message?.command === "openAutofillInlineMenu") { + await this.openInlineMenu(true); } } /** - * Gets the translations for the overlay page. + * Gets the translations for the inline menu page. */ - private getTranslations() { - if (!this.overlayPageTranslations) { - this.overlayPageTranslations = { + private getInlineMenuTranslations() { + if (!this.inlineMenuPageTranslations) { + this.inlineMenuPageTranslations = { locale: BrowserApi.getUILanguage(), opensInANewWindow: this.i18nService.translate("opensInANewWindow"), buttonPageTitle: this.i18nService.translate("bitwardenOverlayButton"), @@ -588,7 +1040,7 @@ class OverlayBackground implements OverlayBackgroundInterface { unlockYourAccount: this.i18nService.translate("unlockYourAccountToViewMatchingLogins"), unlockAccount: this.i18nService.translate("unlockAccount"), fillCredentialsFor: this.i18nService.translate("fillCredentialsFor"), - partialUsername: this.i18nService.translate("partialUsername"), + username: this.i18nService.translate("username")?.toLowerCase(), view: this.i18nService.translate("view"), noItemsToShow: this.i18nService.translate("noItemsToShow"), newItem: this.i18nService.translate("newItem"), @@ -596,17 +1048,17 @@ class OverlayBackground implements OverlayBackgroundInterface { }; } - return this.overlayPageTranslations; + return this.inlineMenuPageTranslations; } /** * Facilitates redirecting focus out of one of the - * overlay elements to elements on the page. + * inline menu elements to elements on the page. * * @param direction - The direction to redirect focus to (either "next", "previous" or "current) * @param sender - The sender of the port message */ - private redirectOverlayFocusOut( + private redirectInlineMenuFocusOut( { direction }: OverlayPortMessage, { sender }: chrome.runtime.Port, ) { @@ -614,9 +1066,9 @@ class OverlayBackground implements OverlayBackgroundInterface { return; } - // 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 - BrowserApi.tabSendMessageData(sender.tab, "redirectOverlayFocusOut", { direction }); + void BrowserApi.tabSendMessageData(sender.tab, "redirectAutofillInlineMenuFocusOut", { + direction, + }); } /** @@ -626,7 +1078,17 @@ class OverlayBackground implements OverlayBackgroundInterface { * @param sender - The sender of the port message */ private getNewVaultItemDetails({ sender }: chrome.runtime.Port) { - void BrowserApi.tabSendMessage(sender.tab, { command: "addNewVaultItemFromOverlay" }); + if (!this.senderTabHasFocusedField(sender)) { + return; + } + + void BrowserApi.tabSendMessage( + sender.tab, + { command: "addNewVaultItemFromOverlay" }, + { + frameId: this.focusedFieldData.frameId || 0, + }, + ); } /** @@ -644,6 +1106,7 @@ class OverlayBackground implements OverlayBackgroundInterface { return; } + this.closeInlineMenu(sender); const uriView = new LoginUriView(); uriView.uri = login.uri; @@ -667,11 +1130,222 @@ class OverlayBackground implements OverlayBackgroundInterface { await BrowserApi.sendMessage("inlineAutofillMenuRefreshAddEditCipher"); } + /** + * Updates the property that identifies if a form field set up for the inline menu is currently focused. + * + * @param message - The message received from the web page + */ + private updateIsFieldCurrentlyFocused(message: OverlayBackgroundExtensionMessage) { + this.isFieldCurrentlyFocused = message.isFieldCurrentlyFocused; + } + + /** + * Allows a content script to check if a form field setup for the inline menu is currently focused. + */ + private checkIsFieldCurrentlyFocused() { + return this.isFieldCurrentlyFocused; + } + + /** + * Updates the property that identifies if a form field is currently being autofilled. + * + * @param message - The message received from the web page + */ + private updateIsFieldCurrentlyFilling(message: OverlayBackgroundExtensionMessage) { + this.isFieldCurrentlyFilling = message.isFieldCurrentlyFilling; + } + + /** + * Allows a content script to check if a form field is currently being autofilled. + */ + private checkIsFieldCurrentlyFilling() { + return this.isFieldCurrentlyFilling; + } + + /** + * Returns the visibility status of the inline menu button. + */ + private checkIsInlineMenuButtonVisible(): boolean { + return this.isInlineMenuButtonVisible; + } + + /** + * Returns the visibility status of the inline menu list. + */ + private checkIsInlineMenuListVisible(): boolean { + return this.isInlineMenuListVisible; + } + + /** + * Responds to the content script's request to check if the inline menu ciphers are populated. + * This will return true only if the sender is the focused field's tab and the inline menu + * ciphers are populated. + * + * @param sender - The sender of the message + */ + private checkIsInlineMenuCiphersPopulated(sender: chrome.runtime.MessageSender) { + return this.senderTabHasFocusedField(sender) && this.inlineMenuCiphers.size > 0; + } + + /** + * Triggers an update in the meta "color-scheme" value within the inline menu button. + * This is done to ensure that the button element has a transparent background, which + * is accomplished by setting the "color-scheme" meta value of the button iframe to + * the same value as the page's meta "color-scheme" value. + */ + private updateInlineMenuButtonColorScheme() { + this.inlineMenuButtonPort?.postMessage({ + command: "updateAutofillInlineMenuColorScheme", + }); + } + + /** + * Triggers an update in the inline menu list's height. + * + * @param message - Contains the dimensions of the inline menu list + */ + private updateInlineMenuListHeight(message: OverlayBackgroundExtensionMessage) { + this.inlineMenuListPort?.postMessage({ + command: "updateAutofillInlineMenuPosition", + styles: message.styles, + }); + } + + /** + * Handles verifying whether the inline menu should be repositioned. This is used to + * guard against removing the inline menu when other frames trigger a resize event. + * + * @param sender - The sender of the message + */ + private checkShouldRepositionInlineMenu(sender: chrome.runtime.MessageSender): boolean { + if (!this.focusedFieldData || !this.senderTabHasFocusedField(sender)) { + return false; + } + + if (this.focusedFieldData?.frameId === sender.frameId) { + return true; + } + + const subFrameOffsetsForTab = this.subFrameOffsetsForTab[sender.tab.id]; + if (subFrameOffsetsForTab) { + for (const value of subFrameOffsetsForTab.values()) { + if (value?.parentFrameIds.includes(sender.frameId)) { + return true; + } + } + } + + return false; + } + + /** + * Identifies if the sender tab is the same as the focused field's tab. + * + * @param sender - The sender of the message + */ + private senderTabHasFocusedField(sender: chrome.runtime.MessageSender) { + return sender.tab.id === this.focusedFieldData?.tabId; + } + + /** + * Triggers when a scroll or resize event occurs within a tab. Will reposition the inline menu + * if the focused field is within the viewport. + * + * @param sender - The sender of the message + */ + private async triggerOverlayReposition(sender: chrome.runtime.MessageSender) { + if (!this.checkShouldRepositionInlineMenu(sender)) { + return; + } + + this.resetFocusedFieldSubFrameOffsets(sender); + this.cancelInlineMenuFadeInAndPositionUpdate(); + void this.toggleInlineMenuHidden({ isInlineMenuHidden: true }, sender); + this.repositionInlineMenuSubject.next(sender); + } + + /** + * Sets the sub frame offsets for the currently focused field's frame to a null value . + * This ensures that we can delay presentation of the inline menu after a reposition + * event if the user clicks on a field before the sub frames can be rebuilt. + * + * @param sender + */ + private resetFocusedFieldSubFrameOffsets(sender: chrome.runtime.MessageSender) { + if (this.focusedFieldData.frameId > 0 && this.subFrameOffsetsForTab[sender.tab.id]) { + this.subFrameOffsetsForTab[sender.tab.id].set(this.focusedFieldData.frameId, null); + } + } + + /** + * Triggers when a focus event occurs within a tab. Will reposition the inline menu + * if the focused field is within the viewport. + * + * @param sender - The sender of the message + */ + private async triggerSubFrameFocusInRebuild(sender: chrome.runtime.MessageSender) { + this.cancelInlineMenuFadeInAndPositionUpdate(); + this.rebuildSubFrameOffsetsSubject.next(sender); + this.repositionInlineMenuSubject.next(sender); + } + + /** + * Handles determining if the inline menu should be repositioned or closed, and initiates + * the process of calculating the new position of the inline menu. + * + * @param sender - The sender of the message + */ + private repositionInlineMenu = async (sender: chrome.runtime.MessageSender) => { + this.cancelInlineMenuFadeInAndPositionUpdate(); + if (!this.isFieldCurrentlyFocused && !this.isInlineMenuButtonVisible) { + await this.closeInlineMenuAfterReposition(sender); + return; + } + + const isFieldWithinViewport = await BrowserApi.tabSendMessage( + sender.tab, + { command: "checkIsMostRecentlyFocusedFieldWithinViewport" }, + { frameId: this.focusedFieldData.frameId }, + ); + if (!isFieldWithinViewport) { + await this.closeInlineMenuAfterReposition(sender); + return; + } + + if (this.focusedFieldData.frameId > 0) { + this.rebuildSubFrameOffsetsSubject.next(sender); + } + + this.startUpdateInlineMenuPositionSubject.next(sender); + }; + + /** + * Triggers a closure of the inline menu during a reposition event. + * + * @param sender - The sender of the message +| */ + private async closeInlineMenuAfterReposition(sender: chrome.runtime.MessageSender) { + await this.toggleInlineMenuHidden( + { isInlineMenuHidden: false, setTransparentInlineMenu: true }, + sender, + ); + this.closeInlineMenu(sender, { forceCloseInlineMenu: true }); + } + + /** + * Cancels the observables that update the position and fade in of the inline menu. + */ + private cancelInlineMenuFadeInAndPositionUpdate() { + this.cancelInlineMenuFadeIn(); + this.cancelUpdateInlineMenuPositionSubject.next(); + } + /** * Sets up the extension message listeners for the overlay. */ - private setupExtensionMessageListeners() { + private setupExtensionListeners() { BrowserApi.messageListener("overlay.background", this.handleExtensionMessage); + BrowserApi.addListener(chrome.webNavigation.onCommitted, this.handleWebNavigationOnCommitted); BrowserApi.addListener(chrome.runtime.onConnect, this.handlePortOnConnect); } @@ -689,18 +1363,42 @@ class OverlayBackground implements OverlayBackgroundInterface { ) => { const handler: CallableFunction | undefined = this.extensionMessageHandlers[message?.command]; if (!handler) { - return; + return null; } const messageResponse = handler({ message, sender }); - if (!messageResponse) { + if (typeof messageResponse === "undefined") { + return null; + } + + Promise.resolve(messageResponse) + .then((response) => sendResponse(response)) + .catch(this.logService.error); + return true; + }; + + /** + * Handles clearing page details and sub frame offsets when a frame or tab navigation event occurs. + * + * @param details - The details of the web navigation event + */ + private handleWebNavigationOnCommitted = ( + details: chrome.webNavigation.WebNavigationTransitionCallbackDetails, + ) => { + const { frameId, tabId } = details; + const subFrames = this.subFrameOffsetsForTab[tabId]; + if (frameId === 0) { + this.removePageDetails(tabId); + if (subFrames) { + subFrames.clear(); + delete this.subFrameOffsetsForTab[tabId]; + } return; } - // 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 - Promise.resolve(messageResponse).then((response) => sendResponse(response)); - return true; + if (subFrames && subFrames.has(frameId)) { + subFrames.delete(frameId); + } }; /** @@ -709,25 +1407,50 @@ class OverlayBackground implements OverlayBackgroundInterface { * @param port - The port that connected to the extension background */ private handlePortOnConnect = async (port: chrome.runtime.Port) => { - const isOverlayListPort = port.name === AutofillOverlayPort.List; - const isOverlayButtonPort = port.name === AutofillOverlayPort.Button; - if (!isOverlayListPort && !isOverlayButtonPort) { + const isInlineMenuListMessageConnector = port.name === AutofillOverlayPort.ListMessageConnector; + const isInlineMenuButtonMessageConnector = + port.name === AutofillOverlayPort.ButtonMessageConnector; + if (isInlineMenuListMessageConnector || isInlineMenuButtonMessageConnector) { + port.onMessage.addListener(this.handleOverlayElementPortMessage); return; } + const isInlineMenuListPort = port.name === AutofillOverlayPort.List; + const isInlineMenuButtonPort = port.name === AutofillOverlayPort.Button; + if (!isInlineMenuListPort && !isInlineMenuButtonPort) { + return; + } + + if (!this.portKeyForTab[port.sender.tab.id]) { + this.portKeyForTab[port.sender.tab.id] = generateRandomChars(12); + } + this.storeOverlayPort(port); + port.onDisconnect.addListener(this.handlePortOnDisconnect); port.onMessage.addListener(this.handleOverlayElementPortMessage); port.postMessage({ - command: `initAutofillOverlay${isOverlayListPort ? "List" : "Button"}`, + command: `initAutofillInlineMenu${isInlineMenuListPort ? "List" : "Button"}`, + iframeUrl: chrome.runtime.getURL( + `overlay/menu-${isInlineMenuListPort ? "list" : "button"}.html`, + ), + pageTitle: chrome.i18n.getMessage( + isInlineMenuListPort ? "bitwardenVault" : "bitwardenOverlayButton", + ), authStatus: await this.getAuthStatus(), - styleSheetUrl: chrome.runtime.getURL(`overlay/${isOverlayListPort ? "list" : "button"}.css`), + styleSheetUrl: chrome.runtime.getURL( + `overlay/menu-${isInlineMenuListPort ? "list" : "button"}.css`, + ), theme: await firstValueFrom(this.themeStateService.selectedTheme$), - translations: this.getTranslations(), - ciphers: isOverlayListPort ? await this.getOverlayCipherData() : null, + translations: this.getInlineMenuTranslations(), + ciphers: isInlineMenuListPort ? await this.getInlineMenuCipherData() : null, + portKey: this.portKeyForTab[port.sender.tab.id], + portName: isInlineMenuListPort + ? AutofillOverlayPort.ListMessageConnector + : AutofillOverlayPort.ButtonMessageConnector, }); - this.updateOverlayPosition( + void this.updateInlineMenuPosition( { - overlayElement: isOverlayListPort + overlayElement: isInlineMenuListPort ? AutofillOverlayElement.List : AutofillOverlayElement.Button, }, @@ -742,14 +1465,14 @@ class OverlayBackground implements OverlayBackgroundInterface { | */ private storeOverlayPort(port: chrome.runtime.Port) { if (port.name === AutofillOverlayPort.List) { - this.storeExpiredOverlayPort(this.overlayListPort); - this.overlayListPort = port; + this.storeExpiredOverlayPort(this.inlineMenuListPort); + this.inlineMenuListPort = port; return; } if (port.name === AutofillOverlayPort.Button) { - this.storeExpiredOverlayPort(this.overlayButtonPort); - this.overlayButtonPort = port; + this.storeExpiredOverlayPort(this.inlineMenuButtonPort); + this.inlineMenuButtonPort = port; } } @@ -776,15 +1499,20 @@ class OverlayBackground implements OverlayBackgroundInterface { message: OverlayBackgroundExtensionMessage, port: chrome.runtime.Port, ) => { - const command = message?.command; - let handler: CallableFunction | undefined; - - if (port.name === AutofillOverlayPort.Button) { - handler = this.overlayButtonPortMessageHandlers[command]; + const tabPortKey = this.portKeyForTab[port.sender.tab.id]; + if (!tabPortKey || tabPortKey !== message?.portKey) { + return; } - if (port.name === AutofillOverlayPort.List) { - handler = this.overlayListPortMessageHandlers[command]; + const command = message.command; + let handler: CallableFunction | undefined; + + if (port.name === AutofillOverlayPort.ButtonMessageConnector) { + handler = this.inlineMenuButtonPortMessageHandlers[command]; + } + + if (port.name === AutofillOverlayPort.ListMessageConnector) { + handler = this.inlineMenuListPortMessageHandlers[command]; } if (!handler) { @@ -793,6 +1521,22 @@ class OverlayBackground implements OverlayBackgroundInterface { handler({ message, port }); }; -} -export default OverlayBackground; + /** + * Ensures that the inline menu list and button port + * references are reset when they are disconnected. + * + * @param port - The port that was disconnected + */ + private handlePortOnDisconnect = (port: chrome.runtime.Port) => { + if (port.name === AutofillOverlayPort.List) { + this.inlineMenuListPort = null; + this.isInlineMenuListVisible = false; + } + + if (port.name === AutofillOverlayPort.Button) { + this.inlineMenuButtonPort = null; + this.isInlineMenuButtonVisible = false; + } + }; +} diff --git a/apps/browser/src/autofill/background/tabs.background.spec.ts b/apps/browser/src/autofill/background/tabs.background.spec.ts index b95e303f17e..4473eb452f3 100644 --- a/apps/browser/src/autofill/background/tabs.background.spec.ts +++ b/apps/browser/src/autofill/background/tabs.background.spec.ts @@ -11,7 +11,7 @@ import { } from "../spec/testing-utils"; import NotificationBackground from "./notification.background"; -import OverlayBackground from "./overlay.background"; +import { OverlayBackground } from "./overlay.background"; import TabsBackground from "./tabs.background"; describe("TabsBackground", () => { @@ -146,6 +146,7 @@ describe("TabsBackground", () => { beforeEach(() => { mainBackground.onUpdatedRan = false; + mainBackground.configService.getFeatureFlag = jest.fn().mockResolvedValue(true); tabsBackground["focusedWindowId"] = focusedWindowId; tab = mock({ windowId: focusedWindowId, @@ -154,18 +155,6 @@ describe("TabsBackground", () => { }); }); - it("removes the cached page details from the overlay background if the tab status is `loading`", () => { - triggerTabOnUpdatedEvent(focusedWindowId, { status: "loading" }, tab); - - expect(overlayBackground.removePageDetails).toHaveBeenCalledWith(focusedWindowId); - }); - - it("removes the cached page details from the overlay background if the tab status is `unloaded`", () => { - triggerTabOnUpdatedEvent(focusedWindowId, { status: "unloaded" }, tab); - - expect(overlayBackground.removePageDetails).toHaveBeenCalledWith(focusedWindowId); - }); - it("skips updating the current tab data the focusedWindowId is set to a value less than zero", async () => { tab.windowId = -1; triggerTabOnUpdatedEvent(focusedWindowId, { status: "loading" }, tab); diff --git a/apps/browser/src/autofill/background/tabs.background.ts b/apps/browser/src/autofill/background/tabs.background.ts index 53c801ff7bc..f68ae6c6edc 100644 --- a/apps/browser/src/autofill/background/tabs.background.ts +++ b/apps/browser/src/autofill/background/tabs.background.ts @@ -1,7 +1,9 @@ +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; + import MainBackground from "../../background/main.background"; +import { OverlayBackground } from "./abstractions/overlay.background"; import NotificationBackground from "./notification.background"; -import OverlayBackground from "./overlay.background"; export default class TabsBackground { constructor( @@ -86,8 +88,11 @@ export default class TabsBackground { changeInfo: chrome.tabs.TabChangeInfo, tab: chrome.tabs.Tab, ) => { + const overlayImprovementsFlag = await this.main.configService.getFeatureFlag( + FeatureFlag.InlineMenuPositioningImprovements, + ); const removePageDetailsStatus = new Set(["loading", "unloaded"]); - if (removePageDetailsStatus.has(changeInfo.status)) { + if (!!overlayImprovementsFlag && removePageDetailsStatus.has(changeInfo.status)) { this.overlayBackground.removePageDetails(tabId); } diff --git a/apps/browser/src/autofill/content/abstractions/autofill-init.ts b/apps/browser/src/autofill/content/abstractions/autofill-init.ts index 91866ffa0bb..8b00b4ecc9e 100644 --- a/apps/browser/src/autofill/content/abstractions/autofill-init.ts +++ b/apps/browser/src/autofill/content/abstractions/autofill-init.ts @@ -1,46 +1,40 @@ import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; +import { AutofillOverlayElementType } from "../../enums/autofill-overlay.enum"; import AutofillScript from "../../models/autofill-script"; -type AutofillExtensionMessage = { +export type AutofillExtensionMessage = { command: string; tab?: chrome.tabs.Tab; sender?: string; fillScript?: AutofillScript; url?: string; + subFrameUrl?: string; + subFrameId?: string; pageDetailsUrl?: string; ciphers?: any; + isInlineMenuHidden?: boolean; + overlayElement?: AutofillOverlayElementType; + isFocusingFieldElement?: boolean; + authStatus?: AuthenticationStatus; + isOpeningFullInlineMenu?: boolean; data?: { - authStatus?: AuthenticationStatus; - isFocusingFieldElement?: boolean; - isOverlayCiphersPopulated?: boolean; - direction?: "previous" | "next"; - isOpeningFullOverlay?: boolean; - forceCloseOverlay?: boolean; - autofillOverlayVisibility?: number; + direction?: "previous" | "next" | "current"; + forceCloseInlineMenu?: boolean; + inlineMenuVisibility?: number; }; }; -type AutofillExtensionMessageParam = { message: AutofillExtensionMessage }; +export type AutofillExtensionMessageParam = { message: AutofillExtensionMessage }; -type AutofillExtensionMessageHandlers = { +export type AutofillExtensionMessageHandlers = { [key: string]: CallableFunction; collectPageDetails: ({ message }: AutofillExtensionMessageParam) => void; collectPageDetailsImmediately: ({ message }: AutofillExtensionMessageParam) => void; fillForm: ({ message }: AutofillExtensionMessageParam) => void; - openAutofillOverlay: ({ message }: AutofillExtensionMessageParam) => void; - closeAutofillOverlay: ({ message }: AutofillExtensionMessageParam) => void; - addNewVaultItemFromOverlay: () => void; - redirectOverlayFocusOut: ({ message }: AutofillExtensionMessageParam) => void; - updateIsOverlayCiphersPopulated: ({ message }: AutofillExtensionMessageParam) => void; - bgUnlockPopoutOpened: () => void; - bgVaultItemRepromptPopoutOpened: () => void; - updateAutofillOverlayVisibility: ({ message }: AutofillExtensionMessageParam) => void; }; -interface AutofillInit { +export interface AutofillInit { init(): void; destroy(): void; } - -export { AutofillExtensionMessage, AutofillExtensionMessageHandlers, AutofillInit }; diff --git a/apps/browser/src/autofill/content/autofill-init.spec.ts b/apps/browser/src/autofill/content/autofill-init.spec.ts index 302b520e336..e27e8ef73d0 100644 --- a/apps/browser/src/autofill/content/autofill-init.spec.ts +++ b/apps/browser/src/autofill/content/autofill-init.spec.ts @@ -1,26 +1,25 @@ -import { mock } from "jest-mock-extended"; - -import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; -import { AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants"; +import { mock, MockProxy } from "jest-mock-extended"; import AutofillPageDetails from "../models/autofill-page-details"; import AutofillScript from "../models/autofill-script"; -import AutofillOverlayContentService from "../services/autofill-overlay-content.service"; +import { AutofillInlineMenuContentService } from "../overlay/inline-menu/content/autofill-inline-menu-content.service"; +import { AutofillOverlayContentService } from "../services/autofill-overlay-content.service"; import { flushPromises, mockQuerySelectorAllDefinedCall, sendMockExtensionMessage, } from "../spec/testing-utils"; -import { RedirectFocusDirection } from "../utils/autofill-overlay.enum"; import { AutofillExtensionMessage } from "./abstractions/autofill-init"; import AutofillInit from "./autofill-init"; describe("AutofillInit", () => { + let inlineMenuElements: MockProxy; + let autofillOverlayContentService: MockProxy; let autofillInit: AutofillInit; - const autofillOverlayContentService = mock(); const originalDocumentReadyState = document.readyState; const mockQuerySelectorAll = mockQuerySelectorAllDefinedCall(); + let sendExtensionMessageSpy: jest.SpyInstance; beforeEach(() => { chrome.runtime.connect = jest.fn().mockReturnValue({ @@ -28,7 +27,12 @@ describe("AutofillInit", () => { addListener: jest.fn(), }, }); - autofillInit = new AutofillInit(autofillOverlayContentService); + inlineMenuElements = mock(); + autofillOverlayContentService = mock(); + autofillInit = new AutofillInit(autofillOverlayContentService, inlineMenuElements); + sendExtensionMessageSpy = jest + .spyOn(autofillInit as any, "sendExtensionMessage") + .mockImplementation(); window.IntersectionObserver = jest.fn(() => mock()); }); @@ -61,13 +65,9 @@ describe("AutofillInit", () => { autofillInit.init(); jest.advanceTimersByTime(250); - expect(chrome.runtime.sendMessage).toHaveBeenCalledWith( - { - command: "bgCollectPageDetails", - sender: "autofillInit", - }, - expect.any(Function), - ); + expect(sendExtensionMessageSpy).toHaveBeenCalledWith("bgCollectPageDetails", { + sender: "autofillInit", + }); }); it("registers a window load listener to collect the page details if the document is not in a `complete` ready state", () => { @@ -106,15 +106,15 @@ describe("AutofillInit", () => { sender = mock(); }); - it("returns a undefined value if a extension message handler is not found with the given message command", () => { + it("returns a null value if a extension message handler is not found with the given message command", () => { message.command = "unknownCommand"; const response = autofillInit["handleExtensionMessage"](message, sender, sendResponse); - expect(response).toBe(undefined); + expect(response).toBe(null); }); - it("returns a undefined value if the message handler does not return a response", async () => { + it("returns a null value if the message handler does not return a response", async () => { const response1 = await autofillInit["handleExtensionMessage"](message, sender, sendResponse); await flushPromises(); @@ -126,7 +126,7 @@ describe("AutofillInit", () => { const response2 = autofillInit["handleExtensionMessage"](message, sender, sendResponse); await flushPromises(); - expect(response2).toBe(undefined); + expect(response2).toBe(null); }); it("returns a true value and calls sendResponse if the message handler returns a response", async () => { @@ -155,6 +155,22 @@ describe("AutofillInit", () => { autofillInit.init(); }); + it("triggers extension message handlers from the AutofillOverlayContentService", () => { + autofillOverlayContentService.messageHandlers.messageHandler = jest.fn(); + + sendMockExtensionMessage({ command: "messageHandler" }, sender, sendResponse); + + expect(autofillOverlayContentService.messageHandlers.messageHandler).toHaveBeenCalled(); + }); + + it("triggers extension message handlers from the AutofillInlineMenuContentService", () => { + inlineMenuElements.messageHandlers.messageHandler = jest.fn(); + + sendMockExtensionMessage({ command: "messageHandler" }, sender, sendResponse); + + expect(inlineMenuElements.messageHandlers.messageHandler).toHaveBeenCalled(); + }); + describe("collectPageDetails", () => { it("sends the collected page details for autofill using a background script message", async () => { const pageDetails: AutofillPageDetails = { @@ -177,8 +193,7 @@ describe("AutofillInit", () => { sendMockExtensionMessage(message, sender, sendResponse); await flushPromises(); - expect(chrome.runtime.sendMessage).toHaveBeenCalledWith({ - command: "collectPageDetailsResponse", + expect(sendExtensionMessageSpy).toHaveBeenCalledWith("collectPageDetailsResponse", { tab: message.tab, details: pageDetails, sender: message.sender, @@ -226,14 +241,11 @@ describe("AutofillInit", () => { }); it("skips calling the InsertAutofillContentService and does not fill the form if the url to fill is not equal to the current tab url", async () => { - const fillScript = mock(); - const message = { + sendMockExtensionMessage({ command: "fillForm", fillScript, pageDetailsUrl: "https://a-different-url.com", - }; - - sendMockExtensionMessage(message); + }); await flushPromises(); expect(autofillInit["insertAutofillContentService"].fillForm).not.toHaveBeenCalledWith( @@ -255,7 +267,10 @@ describe("AutofillInit", () => { }); it("removes the overlay when filling the form", async () => { - const blurAndRemoveOverlaySpy = jest.spyOn(autofillInit as any, "blurAndRemoveOverlay"); + const blurAndRemoveOverlaySpy = jest.spyOn( + autofillInit as any, + "blurFocusedFieldAndCloseInlineMenu", + ); sendMockExtensionMessage({ command: "fillForm", fillScript, @@ -268,10 +283,6 @@ describe("AutofillInit", () => { it("updates the isCurrentlyFilling property of the overlay to true after filling", async () => { jest.useFakeTimers(); - jest.spyOn(autofillInit as any, "updateOverlayIsCurrentlyFilling"); - jest - .spyOn(autofillInit["autofillOverlayContentService"], "focusMostRecentOverlayField") - .mockImplementation(); sendMockExtensionMessage({ command: "fillForm", @@ -281,292 +292,18 @@ describe("AutofillInit", () => { await flushPromises(); jest.advanceTimersByTime(300); - expect(autofillInit["updateOverlayIsCurrentlyFilling"]).toHaveBeenNthCalledWith(1, true); + expect(sendExtensionMessageSpy).toHaveBeenNthCalledWith( + 1, + "updateIsFieldCurrentlyFilling", + { isFieldCurrentlyFilling: true }, + ); expect(autofillInit["insertAutofillContentService"].fillForm).toHaveBeenCalledWith( fillScript, ); - expect(autofillInit["updateOverlayIsCurrentlyFilling"]).toHaveBeenNthCalledWith(2, false); - }); - - it("skips attempting to focus the most recent field if the autofillOverlayContentService is not present", async () => { - jest.useFakeTimers(); - const newAutofillInit = new AutofillInit(undefined); - newAutofillInit.init(); - jest.spyOn(newAutofillInit as any, "updateOverlayIsCurrentlyFilling"); - jest - .spyOn(newAutofillInit["insertAutofillContentService"], "fillForm") - .mockImplementation(); - - sendMockExtensionMessage({ - command: "fillForm", - fillScript, - pageDetailsUrl: window.location.href, - }); - await flushPromises(); - jest.advanceTimersByTime(300); - - expect(newAutofillInit["updateOverlayIsCurrentlyFilling"]).toHaveBeenNthCalledWith( - 1, - true, - ); - expect(newAutofillInit["insertAutofillContentService"].fillForm).toHaveBeenCalledWith( - fillScript, - ); - expect(newAutofillInit["updateOverlayIsCurrentlyFilling"]).not.toHaveBeenNthCalledWith( + expect(sendExtensionMessageSpy).toHaveBeenNthCalledWith( 2, - false, - ); - }); - }); - - describe("openAutofillOverlay", () => { - const message = { - command: "openAutofillOverlay", - data: { - isFocusingFieldElement: true, - isOpeningFullOverlay: true, - authStatus: AuthenticationStatus.Unlocked, - }, - }; - - it("skips attempting to open the autofill overlay if the autofillOverlayContentService is not present", () => { - const newAutofillInit = new AutofillInit(undefined); - newAutofillInit.init(); - - sendMockExtensionMessage(message); - - expect(newAutofillInit["autofillOverlayContentService"]).toBe(undefined); - }); - - it("opens the autofill overlay", () => { - sendMockExtensionMessage(message); - - expect( - autofillInit["autofillOverlayContentService"].openAutofillOverlay, - ).toHaveBeenCalledWith({ - isFocusingFieldElement: message.data.isFocusingFieldElement, - isOpeningFullOverlay: message.data.isOpeningFullOverlay, - authStatus: message.data.authStatus, - }); - }); - }); - - describe("closeAutofillOverlay", () => { - beforeEach(() => { - autofillInit["autofillOverlayContentService"].isFieldCurrentlyFocused = false; - autofillInit["autofillOverlayContentService"].isCurrentlyFilling = false; - }); - - it("skips attempting to remove the overlay if the autofillOverlayContentService is not present", () => { - const newAutofillInit = new AutofillInit(undefined); - newAutofillInit.init(); - jest.spyOn(newAutofillInit as any, "removeAutofillOverlay"); - - sendMockExtensionMessage({ - command: "closeAutofillOverlay", - data: { forceCloseOverlay: false }, - }); - - expect(newAutofillInit["autofillOverlayContentService"]).toBe(undefined); - }); - - it("removes the autofill overlay if the message flags a forced closure", () => { - sendMockExtensionMessage({ - command: "closeAutofillOverlay", - data: { forceCloseOverlay: true }, - }); - - expect( - autofillInit["autofillOverlayContentService"].removeAutofillOverlay, - ).toHaveBeenCalled(); - }); - - it("ignores the message if a field is currently focused", () => { - autofillInit["autofillOverlayContentService"].isFieldCurrentlyFocused = true; - - sendMockExtensionMessage({ command: "closeAutofillOverlay" }); - - expect( - autofillInit["autofillOverlayContentService"].removeAutofillOverlayList, - ).not.toHaveBeenCalled(); - expect( - autofillInit["autofillOverlayContentService"].removeAutofillOverlay, - ).not.toHaveBeenCalled(); - }); - - it("removes the autofill overlay list if the overlay is currently filling", () => { - autofillInit["autofillOverlayContentService"].isCurrentlyFilling = true; - - sendMockExtensionMessage({ command: "closeAutofillOverlay" }); - - expect( - autofillInit["autofillOverlayContentService"].removeAutofillOverlayList, - ).toHaveBeenCalled(); - expect( - autofillInit["autofillOverlayContentService"].removeAutofillOverlay, - ).not.toHaveBeenCalled(); - }); - - it("removes the entire overlay if the overlay is not currently filling", () => { - sendMockExtensionMessage({ command: "closeAutofillOverlay" }); - - expect( - autofillInit["autofillOverlayContentService"].removeAutofillOverlayList, - ).not.toHaveBeenCalled(); - expect( - autofillInit["autofillOverlayContentService"].removeAutofillOverlay, - ).toHaveBeenCalled(); - }); - }); - - describe("addNewVaultItemFromOverlay", () => { - it("will not add a new vault item if the autofillOverlayContentService is not present", () => { - const newAutofillInit = new AutofillInit(undefined); - newAutofillInit.init(); - - sendMockExtensionMessage({ command: "addNewVaultItemFromOverlay" }); - - expect(newAutofillInit["autofillOverlayContentService"]).toBe(undefined); - }); - - it("will add a new vault item", () => { - sendMockExtensionMessage({ command: "addNewVaultItemFromOverlay" }); - - expect(autofillInit["autofillOverlayContentService"].addNewVaultItem).toHaveBeenCalled(); - }); - }); - - describe("redirectOverlayFocusOut", () => { - const message = { - command: "redirectOverlayFocusOut", - data: { - direction: RedirectFocusDirection.Next, - }, - }; - - it("ignores the message to redirect focus if the autofillOverlayContentService does not exist", () => { - const newAutofillInit = new AutofillInit(undefined); - newAutofillInit.init(); - - sendMockExtensionMessage(message); - - expect(newAutofillInit["autofillOverlayContentService"]).toBe(undefined); - }); - - it("redirects the overlay focus", () => { - sendMockExtensionMessage(message); - - expect( - autofillInit["autofillOverlayContentService"].redirectOverlayFocusOut, - ).toHaveBeenCalledWith(message.data.direction); - }); - }); - - describe("updateIsOverlayCiphersPopulated", () => { - const message = { - command: "updateIsOverlayCiphersPopulated", - data: { - isOverlayCiphersPopulated: true, - }, - }; - - it("skips updating whether the ciphers are populated if the autofillOverlayContentService does note exist", () => { - const newAutofillInit = new AutofillInit(undefined); - newAutofillInit.init(); - - sendMockExtensionMessage(message); - - expect(newAutofillInit["autofillOverlayContentService"]).toBe(undefined); - }); - - it("updates whether the overlay ciphers are populated", () => { - sendMockExtensionMessage(message); - - expect(autofillInit["autofillOverlayContentService"].isOverlayCiphersPopulated).toEqual( - message.data.isOverlayCiphersPopulated, - ); - }); - }); - - describe("bgUnlockPopoutOpened", () => { - it("skips attempting to blur and remove the overlay if the autofillOverlayContentService is not present", () => { - const newAutofillInit = new AutofillInit(undefined); - newAutofillInit.init(); - jest.spyOn(newAutofillInit as any, "removeAutofillOverlay"); - - sendMockExtensionMessage({ command: "bgUnlockPopoutOpened" }); - - expect(newAutofillInit["autofillOverlayContentService"]).toBe(undefined); - expect(newAutofillInit["removeAutofillOverlay"]).not.toHaveBeenCalled(); - }); - - it("blurs the most recently focused feel and remove the autofill overlay", () => { - jest.spyOn(autofillInit["autofillOverlayContentService"], "blurMostRecentOverlayField"); - jest.spyOn(autofillInit as any, "removeAutofillOverlay"); - - sendMockExtensionMessage({ command: "bgUnlockPopoutOpened" }); - - expect( - autofillInit["autofillOverlayContentService"].blurMostRecentOverlayField, - ).toHaveBeenCalled(); - expect(autofillInit["removeAutofillOverlay"]).toHaveBeenCalled(); - }); - }); - - describe("bgVaultItemRepromptPopoutOpened", () => { - it("skips attempting to blur and remove the overlay if the autofillOverlayContentService is not present", () => { - const newAutofillInit = new AutofillInit(undefined); - newAutofillInit.init(); - jest.spyOn(newAutofillInit as any, "removeAutofillOverlay"); - - sendMockExtensionMessage({ command: "bgVaultItemRepromptPopoutOpened" }); - - expect(newAutofillInit["autofillOverlayContentService"]).toBe(undefined); - expect(newAutofillInit["removeAutofillOverlay"]).not.toHaveBeenCalled(); - }); - - it("blurs the most recently focused feel and remove the autofill overlay", () => { - jest.spyOn(autofillInit["autofillOverlayContentService"], "blurMostRecentOverlayField"); - jest.spyOn(autofillInit as any, "removeAutofillOverlay"); - - sendMockExtensionMessage({ command: "bgVaultItemRepromptPopoutOpened" }); - - expect( - autofillInit["autofillOverlayContentService"].blurMostRecentOverlayField, - ).toHaveBeenCalled(); - expect(autofillInit["removeAutofillOverlay"]).toHaveBeenCalled(); - }); - }); - - describe("updateAutofillOverlayVisibility", () => { - beforeEach(() => { - autofillInit["autofillOverlayContentService"].autofillOverlayVisibility = - AutofillOverlayVisibility.OnButtonClick; - }); - - it("skips attempting to update the overlay visibility if the autofillOverlayVisibility data value is not present", () => { - sendMockExtensionMessage({ - command: "updateAutofillOverlayVisibility", - data: {}, - }); - - expect(autofillInit["autofillOverlayContentService"].autofillOverlayVisibility).toEqual( - AutofillOverlayVisibility.OnButtonClick, - ); - }); - - it("updates the overlay visibility value", () => { - const message = { - command: "updateAutofillOverlayVisibility", - data: { - autofillOverlayVisibility: AutofillOverlayVisibility.Off, - }, - }; - - sendMockExtensionMessage(message); - - expect(autofillInit["autofillOverlayContentService"].autofillOverlayVisibility).toEqual( - message.data.autofillOverlayVisibility, + "updateIsFieldCurrentlyFilling", + { isFieldCurrentlyFilling: false }, ); }); }); diff --git a/apps/browser/src/autofill/content/autofill-init.ts b/apps/browser/src/autofill/content/autofill-init.ts index e78a1fb5ee1..70f815d2234 100644 --- a/apps/browser/src/autofill/content/autofill-init.ts +++ b/apps/browser/src/autofill/content/autofill-init.ts @@ -1,4 +1,7 @@ +import { EVENTS } from "@bitwarden/common/autofill/constants"; + import AutofillPageDetails from "../models/autofill-page-details"; +import { AutofillInlineMenuContentService } from "../overlay/inline-menu/abstractions/autofill-inline-menu-content.service"; import { AutofillOverlayContentService } from "../services/abstractions/autofill-overlay-content.service"; import CollectAutofillContentService from "../services/collect-autofill-content.service"; import DomElementVisibilityService from "../services/dom-element-visibility.service"; @@ -12,7 +15,9 @@ import { } from "./abstractions/autofill-init"; class AutofillInit implements AutofillInitInterface { + private readonly sendExtensionMessage = sendExtensionMessage; private readonly autofillOverlayContentService: AutofillOverlayContentService | undefined; + private readonly autofillInlineMenuContentService: AutofillInlineMenuContentService | undefined; private readonly domElementVisibilityService: DomElementVisibilityService; private readonly collectAutofillContentService: CollectAutofillContentService; private readonly insertAutofillContentService: InsertAutofillContentService; @@ -21,14 +26,6 @@ class AutofillInit implements AutofillInitInterface { collectPageDetails: ({ message }) => this.collectPageDetails(message), collectPageDetailsImmediately: ({ message }) => this.collectPageDetails(message, true), fillForm: ({ message }) => this.fillForm(message), - openAutofillOverlay: ({ message }) => this.openAutofillOverlay(message), - closeAutofillOverlay: ({ message }) => this.removeAutofillOverlay(message), - addNewVaultItemFromOverlay: () => this.addNewVaultItemFromOverlay(), - redirectOverlayFocusOut: ({ message }) => this.redirectOverlayFocusOut(message), - updateIsOverlayCiphersPopulated: ({ message }) => this.updateIsOverlayCiphersPopulated(message), - bgUnlockPopoutOpened: () => this.blurAndRemoveOverlay(), - bgVaultItemRepromptPopoutOpened: () => this.blurAndRemoveOverlay(), - updateAutofillOverlayVisibility: ({ message }) => this.updateAutofillOverlayVisibility(message), }; /** @@ -36,10 +33,17 @@ class AutofillInit implements AutofillInitInterface { * CollectAutofillContentService and InsertAutofillContentService classes. * * @param autofillOverlayContentService - The autofill overlay content service, potentially undefined. + * @param inlineMenuElements - The inline menu elements, potentially undefined. */ - constructor(autofillOverlayContentService?: AutofillOverlayContentService) { + constructor( + autofillOverlayContentService?: AutofillOverlayContentService, + inlineMenuElements?: AutofillInlineMenuContentService, + ) { this.autofillOverlayContentService = autofillOverlayContentService; - this.domElementVisibilityService = new DomElementVisibilityService(); + this.autofillInlineMenuContentService = inlineMenuElements; + this.domElementVisibilityService = new DomElementVisibilityService( + this.autofillInlineMenuContentService, + ); this.collectAutofillContentService = new CollectAutofillContentService( this.domElementVisibilityService, this.autofillOverlayContentService, @@ -70,7 +74,7 @@ class AutofillInit implements AutofillInitInterface { const sendCollectDetailsMessage = () => { this.clearCollectPageDetailsOnLoadTimeout(); this.collectPageDetailsOnLoadTimeout = setTimeout( - () => sendExtensionMessage("bgCollectPageDetails", { sender: "autofillInit" }), + () => this.sendExtensionMessage("bgCollectPageDetails", { sender: "autofillInit" }), 250, ); }; @@ -79,7 +83,7 @@ class AutofillInit implements AutofillInitInterface { sendCollectDetailsMessage(); } - globalThis.addEventListener("load", sendCollectDetailsMessage); + globalThis.addEventListener(EVENTS.LOAD, sendCollectDetailsMessage); } /** @@ -102,8 +106,7 @@ class AutofillInit implements AutofillInitInterface { return pageDetails; } - void chrome.runtime.sendMessage({ - command: "collectPageDetailsResponse", + void this.sendExtensionMessage("collectPageDetailsResponse", { tab: message.tab, details: pageDetails, sender: message.sender, @@ -120,134 +123,28 @@ class AutofillInit implements AutofillInitInterface { return; } - this.blurAndRemoveOverlay(); - this.updateOverlayIsCurrentlyFilling(true); + this.blurFocusedFieldAndCloseInlineMenu(); + await this.sendExtensionMessage("updateIsFieldCurrentlyFilling", { + isFieldCurrentlyFilling: true, + }); await this.insertAutofillContentService.fillForm(fillScript); - if (!this.autofillOverlayContentService) { - return; - } - - setTimeout(() => this.updateOverlayIsCurrentlyFilling(false), 250); - } - - /** - * Handles updating the overlay is currently filling value. - * - * @param isCurrentlyFilling - Indicates if the overlay is currently filling - */ - private updateOverlayIsCurrentlyFilling(isCurrentlyFilling: boolean) { - if (!this.autofillOverlayContentService) { - return; - } - - this.autofillOverlayContentService.isCurrentlyFilling = isCurrentlyFilling; - } - - /** - * Opens the autofill overlay. - * - * @param data - The extension message data. - */ - private openAutofillOverlay({ data }: AutofillExtensionMessage) { - if (!this.autofillOverlayContentService) { - return; - } - - this.autofillOverlayContentService.openAutofillOverlay(data); - } - - /** - * Blurs the most recent overlay field and removes the overlay. Used - * in cases where the background unlock or vault item reprompt popout - * is opened. - */ - private blurAndRemoveOverlay() { - if (!this.autofillOverlayContentService) { - return; - } - - this.autofillOverlayContentService.blurMostRecentOverlayField(); - this.removeAutofillOverlay(); - } - - /** - * Removes the autofill overlay if the field is not currently focused. - * If the autofill is currently filling, only the overlay list will be - * removed. - */ - private removeAutofillOverlay(message?: AutofillExtensionMessage) { - if (message?.data?.forceCloseOverlay) { - this.autofillOverlayContentService?.removeAutofillOverlay(); - return; - } - - if ( - !this.autofillOverlayContentService || - this.autofillOverlayContentService.isFieldCurrentlyFocused - ) { - return; - } - - if (this.autofillOverlayContentService.isCurrentlyFilling) { - this.autofillOverlayContentService.removeAutofillOverlayList(); - return; - } - - this.autofillOverlayContentService.removeAutofillOverlay(); - } - - /** - * Adds a new vault item from the overlay. - */ - private addNewVaultItemFromOverlay() { - if (!this.autofillOverlayContentService) { - return; - } - - this.autofillOverlayContentService.addNewVaultItem(); - } - - /** - * Redirects the overlay focus out of an overlay iframe. - * - * @param data - Contains the direction to redirect the focus. - */ - private redirectOverlayFocusOut({ data }: AutofillExtensionMessage) { - if (!this.autofillOverlayContentService) { - return; - } - - this.autofillOverlayContentService.redirectOverlayFocusOut(data?.direction); - } - - /** - * Updates whether the current tab has ciphers that can populate the overlay list - * - * @param data - Contains the isOverlayCiphersPopulated value - * - */ - private updateIsOverlayCiphersPopulated({ data }: AutofillExtensionMessage) { - if (!this.autofillOverlayContentService) { - return; - } - - this.autofillOverlayContentService.isOverlayCiphersPopulated = Boolean( - data?.isOverlayCiphersPopulated, + setTimeout( + () => + this.sendExtensionMessage("updateIsFieldCurrentlyFilling", { + isFieldCurrentlyFilling: false, + }), + 250, ); } /** - * Updates the autofill overlay visibility. - * - * @param data - Contains the autoFillOverlayVisibility value + * Blurs the most recently focused field and removes the inline menu. Used + * in cases where the background unlock or vault item reprompt popout + * is opened. */ - private updateAutofillOverlayVisibility({ data }: AutofillExtensionMessage) { - if (!this.autofillOverlayContentService || isNaN(data?.autofillOverlayVisibility)) { - return; - } - - this.autofillOverlayContentService.autofillOverlayVisibility = data?.autofillOverlayVisibility; + private blurFocusedFieldAndCloseInlineMenu() { + this.autofillOverlayContentService?.blurMostRecentlyFocusedField(true); } /** @@ -279,22 +176,37 @@ class AutofillInit implements AutofillInitInterface { sendResponse: (response?: any) => void, ): boolean => { const command: string = message.command; - const handler: CallableFunction | undefined = this.extensionMessageHandlers[command]; + const handler: CallableFunction | undefined = this.getExtensionMessageHandler(command); if (!handler) { - return; + return null; } const messageResponse = handler({ message, sender }); - if (!messageResponse) { - return; + if (typeof messageResponse === "undefined") { + return null; } - // 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 - Promise.resolve(messageResponse).then((response) => sendResponse(response)); + void Promise.resolve(messageResponse).then((response) => sendResponse(response)); return true; }; + /** + * Gets the extension message handler for the given command. + * + * @param command - The extension message command. + */ + private getExtensionMessageHandler(command: string): CallableFunction | undefined { + if (this.autofillOverlayContentService?.messageHandlers?.[command]) { + return this.autofillOverlayContentService.messageHandlers[command]; + } + + if (this.autofillInlineMenuContentService?.messageHandlers?.[command]) { + return this.autofillInlineMenuContentService.messageHandlers[command]; + } + + return this.extensionMessageHandlers[command]; + } + /** * Handles destroying the autofill init content script. Removes all * listeners, timeouts, and object instances to prevent memory leaks. @@ -304,6 +216,7 @@ class AutofillInit implements AutofillInitInterface { chrome.runtime.onMessage.removeListener(this.handleExtensionMessage); this.collectAutofillContentService.destroy(); this.autofillOverlayContentService?.destroy(); + this.autofillInlineMenuContentService?.destroy(); } } diff --git a/apps/browser/src/autofill/content/bootstrap-autofill-overlay.ts b/apps/browser/src/autofill/content/bootstrap-autofill-overlay.ts index ab21e367c29..22430227660 100644 --- a/apps/browser/src/autofill/content/bootstrap-autofill-overlay.ts +++ b/apps/browser/src/autofill/content/bootstrap-autofill-overlay.ts @@ -1,12 +1,24 @@ -import AutofillOverlayContentService from "../services/autofill-overlay-content.service"; +import { AutofillInlineMenuContentService } from "../overlay/inline-menu/content/autofill-inline-menu-content.service"; +import { AutofillOverlayContentService } from "../services/autofill-overlay-content.service"; +import { InlineMenuFieldQualificationService } from "../services/inline-menu-field-qualification.service"; import { setupAutofillInitDisconnectAction } from "../utils"; import AutofillInit from "./autofill-init"; (function (windowContext) { if (!windowContext.bitwardenAutofillInit) { - const autofillOverlayContentService = new AutofillOverlayContentService(); - windowContext.bitwardenAutofillInit = new AutofillInit(autofillOverlayContentService); + const inlineMenuFieldQualificationService = new InlineMenuFieldQualificationService(); + const autofillOverlayContentService = new AutofillOverlayContentService( + inlineMenuFieldQualificationService, + ); + let inlineMenuElements: AutofillInlineMenuContentService; + if (globalThis.self === globalThis.top) { + inlineMenuElements = new AutofillInlineMenuContentService(); + } + windowContext.bitwardenAutofillInit = new AutofillInit( + autofillOverlayContentService, + inlineMenuElements, + ); setupAutofillInitDisconnectAction(windowContext); windowContext.bitwardenAutofillInit.init(); diff --git a/apps/browser/src/autofill/deprecated/background/abstractions/overlay.background.deprecated.ts b/apps/browser/src/autofill/deprecated/background/abstractions/overlay.background.deprecated.ts new file mode 100644 index 00000000000..88b78dc2495 --- /dev/null +++ b/apps/browser/src/autofill/deprecated/background/abstractions/overlay.background.deprecated.ts @@ -0,0 +1,124 @@ +import { CipherType } from "@bitwarden/common/vault/enums"; +import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type"; + +import { LockedVaultPendingNotificationsData } from "../../../background/abstractions/notification.background"; +import AutofillPageDetails from "../../../models/autofill-page-details"; + +type WebsiteIconData = { + imageEnabled: boolean; + image: string; + fallbackImage: string; + icon: string; +}; + +type OverlayAddNewItemMessage = { + login?: { + uri?: string; + hostname: string; + username: string; + password: string; + }; +}; + +type OverlayBackgroundExtensionMessage = { + [key: string]: any; + command: string; + tab?: chrome.tabs.Tab; + sender?: string; + details?: AutofillPageDetails; + overlayElement?: string; + display?: string; + data?: LockedVaultPendingNotificationsData; +} & OverlayAddNewItemMessage; + +type OverlayPortMessage = { + [key: string]: any; + command: string; + direction?: string; + overlayCipherId?: string; +}; + +type FocusedFieldData = { + focusedFieldStyles: Partial; + focusedFieldRects: Partial; + tabId?: number; +}; + +type OverlayCipherData = { + id: string; + name: string; + type: CipherType; + reprompt: CipherRepromptType; + favorite: boolean; + icon: { imageEnabled: boolean; image: string; fallbackImage: string; icon: string }; + login?: { username: string }; + card?: string; +}; + +type BackgroundMessageParam = { + message: OverlayBackgroundExtensionMessage; +}; +type BackgroundSenderParam = { + sender: chrome.runtime.MessageSender; +}; +type BackgroundOnMessageHandlerParams = BackgroundMessageParam & BackgroundSenderParam; + +type OverlayBackgroundExtensionMessageHandlers = { + [key: string]: CallableFunction; + openAutofillOverlay: () => void; + autofillOverlayElementClosed: ({ message, sender }: BackgroundOnMessageHandlerParams) => void; + autofillOverlayAddNewVaultItem: ({ message, sender }: BackgroundOnMessageHandlerParams) => void; + getAutofillOverlayVisibility: () => void; + checkAutofillOverlayFocused: () => void; + focusAutofillOverlayList: () => void; + updateAutofillOverlayPosition: ({ message, sender }: BackgroundOnMessageHandlerParams) => void; + updateAutofillOverlayHidden: ({ message }: BackgroundMessageParam) => void; + updateFocusedFieldData: ({ message, sender }: BackgroundOnMessageHandlerParams) => void; + collectPageDetailsResponse: ({ message, sender }: BackgroundOnMessageHandlerParams) => void; + unlockCompleted: ({ message }: BackgroundMessageParam) => void; + addedCipher: () => void; + addEditCipherSubmitted: () => void; + editedCipher: () => void; + deletedCipher: () => void; +}; + +type PortMessageParam = { + message: OverlayPortMessage; +}; +type PortConnectionParam = { + port: chrome.runtime.Port; +}; +type PortOnMessageHandlerParams = PortMessageParam & PortConnectionParam; + +type OverlayButtonPortMessageHandlers = { + [key: string]: CallableFunction; + overlayButtonClicked: ({ port }: PortConnectionParam) => void; + closeAutofillOverlay: ({ port }: PortConnectionParam) => void; + forceCloseAutofillOverlay: ({ port }: PortConnectionParam) => void; + overlayPageBlurred: () => void; + redirectOverlayFocusOut: ({ message, port }: PortOnMessageHandlerParams) => void; +}; + +type OverlayListPortMessageHandlers = { + [key: string]: CallableFunction; + checkAutofillOverlayButtonFocused: () => void; + forceCloseAutofillOverlay: ({ port }: PortConnectionParam) => void; + overlayPageBlurred: () => void; + unlockVault: ({ port }: PortConnectionParam) => void; + fillSelectedListItem: ({ message, port }: PortOnMessageHandlerParams) => void; + addNewVaultItem: ({ port }: PortConnectionParam) => void; + viewSelectedCipher: ({ message, port }: PortOnMessageHandlerParams) => void; + redirectOverlayFocusOut: ({ message, port }: PortOnMessageHandlerParams) => void; +}; + +export { + WebsiteIconData, + OverlayBackgroundExtensionMessage, + OverlayPortMessage, + FocusedFieldData, + OverlayCipherData, + OverlayAddNewItemMessage, + OverlayBackgroundExtensionMessageHandlers, + OverlayButtonPortMessageHandlers, + OverlayListPortMessageHandlers, +}; diff --git a/apps/browser/src/autofill/deprecated/background/overlay.background.deprecated.spec.ts b/apps/browser/src/autofill/deprecated/background/overlay.background.deprecated.spec.ts new file mode 100644 index 00000000000..c3285059c7e --- /dev/null +++ b/apps/browser/src/autofill/deprecated/background/overlay.background.deprecated.spec.ts @@ -0,0 +1,1463 @@ +import { mock, MockProxy, mockReset } from "jest-mock-extended"; +import { BehaviorSubject, of } from "rxjs"; + +import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; +import { AuthService } from "@bitwarden/common/auth/services/auth.service"; +import { + SHOW_AUTOFILL_BUTTON, + AutofillOverlayVisibility, +} from "@bitwarden/common/autofill/constants"; +import { AutofillSettingsService } from "@bitwarden/common/autofill/services/autofill-settings.service"; +import { + DefaultDomainSettingsService, + DomainSettingsService, +} from "@bitwarden/common/autofill/services/domain-settings.service"; +import { + EnvironmentService, + Region, +} from "@bitwarden/common/platform/abstractions/environment.service"; +import { ThemeType } from "@bitwarden/common/platform/enums"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { CloudEnvironment } from "@bitwarden/common/platform/services/default-environment.service"; +import { I18nService } from "@bitwarden/common/platform/services/i18n.service"; +import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; +import { + FakeStateProvider, + FakeAccountService, + mockAccountServiceWith, +} from "@bitwarden/common/spec"; +import { UserId } from "@bitwarden/common/types/guid"; +import { CipherType } from "@bitwarden/common/vault/enums"; +import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { CipherService } from "@bitwarden/common/vault/services/cipher.service"; + +import { BrowserApi } from "../../../platform/browser/browser-api"; +import { BrowserPlatformUtilsService } from "../../../platform/services/platform-utils/browser-platform-utils.service"; +import { + AutofillOverlayElement, + AutofillOverlayPort, + RedirectFocusDirection, +} from "../../enums/autofill-overlay.enum"; +import { AutofillService } from "../../services/abstractions/autofill.service"; +import { + createAutofillPageDetailsMock, + createChromeTabMock, + createFocusedFieldDataMock, + createPageDetailMock, + createPortSpyMock, +} from "../../spec/autofill-mocks"; +import { flushPromises, sendMockExtensionMessage, sendPortMessage } from "../../spec/testing-utils"; + +import LegacyOverlayBackground from "./overlay.background.deprecated"; + +describe("OverlayBackground", () => { + const mockUserId = Utils.newGuid() as UserId; + const accountService: FakeAccountService = mockAccountServiceWith(mockUserId); + const fakeStateProvider: FakeStateProvider = new FakeStateProvider(accountService); + let domainSettingsService: DomainSettingsService; + let buttonPortSpy: chrome.runtime.Port; + let listPortSpy: chrome.runtime.Port; + let overlayBackground: LegacyOverlayBackground; + const cipherService = mock(); + const autofillService = mock(); + let activeAccountStatusMock$: BehaviorSubject; + let authService: MockProxy; + + const environmentService = mock(); + environmentService.environment$ = new BehaviorSubject( + new CloudEnvironment({ + key: Region.US, + domain: "bitwarden.com", + urls: { icons: "https://icons.bitwarden.com/" }, + }), + ); + const autofillSettingsService = mock(); + const i18nService = mock(); + const platformUtilsService = mock(); + const themeStateService = mock(); + const initOverlayElementPorts = async (options = { initList: true, initButton: true }) => { + const { initList, initButton } = options; + if (initButton) { + await overlayBackground["handlePortOnConnect"](createPortSpyMock(AutofillOverlayPort.Button)); + buttonPortSpy = overlayBackground["overlayButtonPort"]; + } + + if (initList) { + await overlayBackground["handlePortOnConnect"](createPortSpyMock(AutofillOverlayPort.List)); + listPortSpy = overlayBackground["overlayListPort"]; + } + + return { buttonPortSpy, listPortSpy }; + }; + + beforeEach(() => { + domainSettingsService = new DefaultDomainSettingsService(fakeStateProvider); + activeAccountStatusMock$ = new BehaviorSubject(AuthenticationStatus.Unlocked); + authService = mock(); + authService.activeAccountStatus$ = activeAccountStatusMock$; + overlayBackground = new LegacyOverlayBackground( + cipherService, + autofillService, + authService, + environmentService, + domainSettingsService, + autofillSettingsService, + i18nService, + platformUtilsService, + themeStateService, + ); + + jest + .spyOn(overlayBackground as any, "getOverlayVisibility") + .mockResolvedValue(AutofillOverlayVisibility.OnFieldFocus); + + themeStateService.selectedTheme$ = of(ThemeType.Light); + domainSettingsService.showFavicons$ = of(true); + + void overlayBackground.init(); + }); + + afterEach(() => { + jest.clearAllMocks(); + mockReset(cipherService); + }); + + describe("removePageDetails", () => { + it("removes the page details for a specific tab from the pageDetailsForTab object", () => { + const tabId = 1; + const frameId = 2; + overlayBackground["pageDetailsForTab"][tabId] = new Map([[frameId, createPageDetailMock()]]); + overlayBackground.removePageDetails(tabId); + + expect(overlayBackground["pageDetailsForTab"][tabId]).toBeUndefined(); + }); + }); + + describe("init", () => { + it("sets up the extension message listeners, get the overlay's visibility settings, and get the user's auth status", async () => { + overlayBackground["setupExtensionMessageListeners"] = jest.fn(); + overlayBackground["getOverlayVisibility"] = jest.fn(); + overlayBackground["getAuthStatus"] = jest.fn(); + + await overlayBackground.init(); + + expect(overlayBackground["setupExtensionMessageListeners"]).toHaveBeenCalled(); + expect(overlayBackground["getOverlayVisibility"]).toHaveBeenCalled(); + expect(overlayBackground["getAuthStatus"]).toHaveBeenCalled(); + }); + }); + + describe("updateOverlayCiphers", () => { + const url = "https://jest-testing-website.com"; + const tab = createChromeTabMock({ url }); + const cipher1 = mock({ + id: "id-1", + localData: { lastUsedDate: 222 }, + name: "name-1", + type: CipherType.Login, + login: { username: "username-1", uri: url }, + }); + const cipher2 = mock({ + id: "id-2", + localData: { lastUsedDate: 111 }, + name: "name-2", + type: CipherType.Login, + login: { username: "username-2", uri: url }, + }); + + beforeEach(() => { + activeAccountStatusMock$.next(AuthenticationStatus.Unlocked); + }); + + it("ignores updating the overlay ciphers if the user's auth status is not unlocked", async () => { + activeAccountStatusMock$.next(AuthenticationStatus.Locked); + jest.spyOn(BrowserApi, "getTabFromCurrentWindowId"); + jest.spyOn(cipherService, "getAllDecryptedForUrl"); + + await overlayBackground.updateOverlayCiphers(); + + expect(BrowserApi.getTabFromCurrentWindowId).not.toHaveBeenCalled(); + expect(cipherService.getAllDecryptedForUrl).not.toHaveBeenCalled(); + }); + + it("ignores updating the overlay ciphers if the tab is undefined", async () => { + jest.spyOn(BrowserApi, "getTabFromCurrentWindowId").mockResolvedValueOnce(undefined); + jest.spyOn(cipherService, "getAllDecryptedForUrl"); + + await overlayBackground.updateOverlayCiphers(); + + expect(BrowserApi.getTabFromCurrentWindowId).toHaveBeenCalled(); + expect(cipherService.getAllDecryptedForUrl).not.toHaveBeenCalled(); + }); + + it("queries all ciphers for the given url, sort them by last used, and format them for usage in the overlay", async () => { + jest.spyOn(BrowserApi, "getTabFromCurrentWindowId").mockResolvedValueOnce(tab); + cipherService.getAllDecryptedForUrl.mockResolvedValue([cipher1, cipher2]); + cipherService.sortCiphersByLastUsedThenName.mockReturnValue(-1); + jest.spyOn(BrowserApi, "tabSendMessageData").mockImplementation(); + jest.spyOn(overlayBackground as any, "getOverlayCipherData"); + + await overlayBackground.updateOverlayCiphers(); + + expect(BrowserApi.getTabFromCurrentWindowId).toHaveBeenCalled(); + expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledWith(url); + expect(overlayBackground["cipherService"].sortCiphersByLastUsedThenName).toHaveBeenCalled(); + expect(overlayBackground["overlayLoginCiphers"]).toStrictEqual( + new Map([ + ["overlay-cipher-0", cipher2], + ["overlay-cipher-1", cipher1], + ]), + ); + expect(overlayBackground["getOverlayCipherData"]).toHaveBeenCalled(); + }); + + it("posts an `updateOverlayListCiphers` message to the overlay list port, and send a `updateIsOverlayCiphersPopulated` message to the tab indicating that the list of ciphers is populated", async () => { + overlayBackground["overlayListPort"] = mock(); + cipherService.getAllDecryptedForUrl.mockResolvedValue([cipher1, cipher2]); + cipherService.sortCiphersByLastUsedThenName.mockReturnValue(-1); + jest.spyOn(BrowserApi, "getTabFromCurrentWindowId").mockResolvedValueOnce(tab); + jest.spyOn(BrowserApi, "tabSendMessageData").mockImplementation(); + + await overlayBackground.updateOverlayCiphers(); + + expect(overlayBackground["overlayListPort"].postMessage).toHaveBeenCalledWith({ + command: "updateOverlayListCiphers", + ciphers: [ + { + card: null, + favorite: cipher2.favorite, + icon: { + fallbackImage: "images/bwi-globe.png", + icon: "bwi-globe", + image: "https://icons.bitwarden.com//jest-testing-website.com/icon.png", + imageEnabled: true, + }, + id: "overlay-cipher-0", + login: { + username: "username-2", + }, + name: "name-2", + reprompt: cipher2.reprompt, + type: 1, + }, + { + card: null, + favorite: cipher1.favorite, + icon: { + fallbackImage: "images/bwi-globe.png", + icon: "bwi-globe", + image: "https://icons.bitwarden.com//jest-testing-website.com/icon.png", + imageEnabled: true, + }, + id: "overlay-cipher-1", + login: { + username: "username-1", + }, + name: "name-1", + reprompt: cipher1.reprompt, + type: 1, + }, + ], + }); + expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith( + tab, + "updateIsOverlayCiphersPopulated", + { isOverlayCiphersPopulated: true }, + ); + }); + }); + + describe("getOverlayCipherData", () => { + const url = "https://jest-testing-website.com"; + const cipher1 = mock({ + id: "id-1", + localData: { lastUsedDate: 222 }, + name: "name-1", + type: CipherType.Login, + login: { username: "username-1", uri: url }, + }); + const cipher2 = mock({ + id: "id-2", + localData: { lastUsedDate: 111 }, + name: "name-2", + type: CipherType.Login, + login: { username: "username-2", uri: url }, + }); + const cipher3 = mock({ + id: "id-3", + localData: { lastUsedDate: 333 }, + name: "name-3", + type: CipherType.Card, + card: { subTitle: "Visa, *6789" }, + }); + const cipher4 = mock({ + id: "id-4", + localData: { lastUsedDate: 444 }, + name: "name-4", + type: CipherType.Card, + card: { subTitle: "Mastercard, *1234" }, + }); + + it("formats and returns the cipher data", async () => { + overlayBackground["overlayLoginCiphers"] = new Map([ + ["overlay-cipher-0", cipher2], + ["overlay-cipher-1", cipher1], + ["overlay-cipher-2", cipher3], + ["overlay-cipher-3", cipher4], + ]); + + const overlayCipherData = await overlayBackground["getOverlayCipherData"](); + + expect(overlayCipherData).toStrictEqual([ + { + card: null, + favorite: cipher2.favorite, + icon: { + fallbackImage: "images/bwi-globe.png", + icon: "bwi-globe", + image: "https://icons.bitwarden.com//jest-testing-website.com/icon.png", + imageEnabled: true, + }, + id: "overlay-cipher-0", + login: { + username: "username-2", + }, + name: "name-2", + reprompt: cipher2.reprompt, + type: 1, + }, + { + card: null, + favorite: cipher1.favorite, + icon: { + fallbackImage: "images/bwi-globe.png", + icon: "bwi-globe", + image: "https://icons.bitwarden.com//jest-testing-website.com/icon.png", + imageEnabled: true, + }, + id: "overlay-cipher-1", + login: { + username: "username-1", + }, + name: "name-1", + reprompt: cipher1.reprompt, + type: 1, + }, + { + card: "Visa, *6789", + favorite: cipher3.favorite, + icon: { + fallbackImage: "", + icon: "bwi-credit-card", + image: undefined, + imageEnabled: true, + }, + id: "overlay-cipher-2", + login: null, + name: "name-3", + reprompt: cipher3.reprompt, + type: 3, + }, + { + card: "Mastercard, *1234", + favorite: cipher4.favorite, + icon: { + fallbackImage: "", + icon: "bwi-credit-card", + image: undefined, + imageEnabled: true, + }, + id: "overlay-cipher-3", + login: null, + name: "name-4", + reprompt: cipher4.reprompt, + type: 3, + }, + ]); + }); + }); + + describe("getAuthStatus", () => { + it("will update the user's auth status but will not update the overlay ciphers", async () => { + const authStatus = AuthenticationStatus.Unlocked; + overlayBackground["userAuthStatus"] = AuthenticationStatus.Unlocked; + jest.spyOn(overlayBackground["authService"], "getAuthStatus").mockResolvedValue(authStatus); + jest.spyOn(overlayBackground as any, "updateOverlayButtonAuthStatus").mockImplementation(); + jest.spyOn(overlayBackground as any, "updateOverlayCiphers").mockImplementation(); + + const status = await overlayBackground["getAuthStatus"](); + + expect(overlayBackground["authService"].getAuthStatus).toHaveBeenCalled(); + expect(overlayBackground["updateOverlayButtonAuthStatus"]).not.toHaveBeenCalled(); + expect(overlayBackground["updateOverlayCiphers"]).not.toHaveBeenCalled(); + expect(overlayBackground["userAuthStatus"]).toBe(authStatus); + expect(status).toBe(authStatus); + }); + + it("will update the user's auth status and update the overlay ciphers if the status has been modified", async () => { + const authStatus = AuthenticationStatus.Unlocked; + overlayBackground["userAuthStatus"] = AuthenticationStatus.LoggedOut; + jest.spyOn(overlayBackground["authService"], "getAuthStatus").mockResolvedValue(authStatus); + jest.spyOn(overlayBackground as any, "updateOverlayButtonAuthStatus").mockImplementation(); + jest.spyOn(overlayBackground as any, "updateOverlayCiphers").mockImplementation(); + + await overlayBackground["getAuthStatus"](); + + expect(overlayBackground["authService"].getAuthStatus).toHaveBeenCalled(); + expect(overlayBackground["updateOverlayButtonAuthStatus"]).toHaveBeenCalled(); + expect(overlayBackground["updateOverlayCiphers"]).toHaveBeenCalled(); + expect(overlayBackground["userAuthStatus"]).toBe(authStatus); + }); + }); + + describe("updateOverlayButtonAuthStatus", () => { + it("will send a message to the button port with the user's auth status", () => { + overlayBackground["overlayButtonPort"] = mock(); + jest.spyOn(overlayBackground["overlayButtonPort"], "postMessage"); + + overlayBackground["updateOverlayButtonAuthStatus"](); + + expect(overlayBackground["overlayButtonPort"].postMessage).toHaveBeenCalledWith({ + command: "updateOverlayButtonAuthStatus", + authStatus: overlayBackground["userAuthStatus"], + }); + }); + }); + + describe("getTranslations", () => { + it("will query the overlay page translations if they have not been queried", () => { + overlayBackground["overlayPageTranslations"] = undefined; + jest.spyOn(overlayBackground as any, "getTranslations"); + jest.spyOn(overlayBackground["i18nService"], "translate").mockImplementation((key) => key); + jest.spyOn(BrowserApi, "getUILanguage").mockReturnValue("en"); + + const translations = overlayBackground["getTranslations"](); + + expect(overlayBackground["getTranslations"]).toHaveBeenCalled(); + const translationKeys = [ + "opensInANewWindow", + "bitwardenOverlayButton", + "toggleBitwardenVaultOverlay", + "bitwardenVault", + "unlockYourAccountToViewMatchingLogins", + "unlockAccount", + "fillCredentialsFor", + "partialUsername", + "view", + "noItemsToShow", + "newItem", + "addNewVaultItem", + ]; + translationKeys.forEach((key) => { + expect(overlayBackground["i18nService"].translate).toHaveBeenCalledWith(key); + }); + expect(translations).toStrictEqual({ + locale: "en", + opensInANewWindow: "opensInANewWindow", + buttonPageTitle: "bitwardenOverlayButton", + toggleBitwardenVaultOverlay: "toggleBitwardenVaultOverlay", + listPageTitle: "bitwardenVault", + unlockYourAccount: "unlockYourAccountToViewMatchingLogins", + unlockAccount: "unlockAccount", + fillCredentialsFor: "fillCredentialsFor", + partialUsername: "partialUsername", + view: "view", + noItemsToShow: "noItemsToShow", + newItem: "newItem", + addNewVaultItem: "addNewVaultItem", + }); + }); + }); + + describe("setupExtensionMessageListeners", () => { + it("will set up onMessage and onConnect listeners", () => { + overlayBackground["setupExtensionMessageListeners"](); + + // eslint-disable-next-line + expect(chrome.runtime.onMessage.addListener).toHaveBeenCalled(); + expect(chrome.runtime.onConnect.addListener).toHaveBeenCalled(); + }); + }); + + describe("handleExtensionMessage", () => { + it("will return early if the message command is not present within the extensionMessageHandlers", () => { + const message = { + command: "not-a-command", + }; + const sender = mock({ tab: { id: 1 } }); + const sendResponse = jest.fn(); + + const returnValue = overlayBackground["handleExtensionMessage"]( + message, + sender, + sendResponse, + ); + + expect(returnValue).toBe(null); + expect(sendResponse).not.toHaveBeenCalled(); + }); + + it("will trigger the message handler and return undefined if the message does not have a response", () => { + const message = { + command: "autofillOverlayElementClosed", + }; + const sender = mock({ tab: { id: 1 } }); + const sendResponse = jest.fn(); + jest.spyOn(overlayBackground as any, "overlayElementClosed"); + + const returnValue = overlayBackground["handleExtensionMessage"]( + message, + sender, + sendResponse, + ); + + expect(returnValue).toBe(null); + expect(sendResponse).not.toHaveBeenCalled(); + expect(overlayBackground["overlayElementClosed"]).toHaveBeenCalledWith(message, sender); + }); + + it("will return a response if the message handler returns a response", async () => { + const message = { + command: "openAutofillOverlay", + }; + const sender = mock({ tab: { id: 1 } }); + const sendResponse = jest.fn(); + jest.spyOn(overlayBackground as any, "getTranslations").mockReturnValue("translations"); + + const returnValue = overlayBackground["handleExtensionMessage"]( + message, + sender, + sendResponse, + ); + + expect(returnValue).toBe(true); + }); + + describe("extension message handlers", () => { + beforeEach(() => { + jest + .spyOn(overlayBackground as any, "getAuthStatus") + .mockResolvedValue(AuthenticationStatus.Unlocked); + }); + + describe("openAutofillOverlay message handler", () => { + it("opens the autofill overlay by sending a message to the current tab", async () => { + const sender = mock({ tab: { id: 1 } }); + jest.spyOn(BrowserApi, "getTabFromCurrentWindowId").mockResolvedValueOnce(sender.tab); + jest.spyOn(BrowserApi, "tabSendMessageData").mockImplementation(); + + sendMockExtensionMessage({ command: "openAutofillOverlay" }); + await flushPromises(); + + expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith( + sender.tab, + "openAutofillOverlay", + { + isFocusingFieldElement: false, + isOpeningFullOverlay: false, + authStatus: AuthenticationStatus.Unlocked, + }, + ); + }); + }); + + describe("autofillOverlayElementClosed message handler", () => { + beforeEach(async () => { + await initOverlayElementPorts(); + }); + + it("disconnects any expired ports if the sender is not from the same page as the most recently focused field", () => { + const port1 = mock(); + const port2 = mock(); + overlayBackground["expiredPorts"] = [port1, port2]; + const sender = mock({ tab: { id: 1 } }); + const focusedFieldData = createFocusedFieldDataMock({ tabId: 2 }); + sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); + + sendMockExtensionMessage( + { + command: "autofillOverlayElementClosed", + overlayElement: AutofillOverlayElement.Button, + }, + sender, + ); + + expect(port1.disconnect).toHaveBeenCalled(); + expect(port2.disconnect).toHaveBeenCalled(); + }); + + it("disconnects the button element port", () => { + sendMockExtensionMessage({ + command: "autofillOverlayElementClosed", + overlayElement: AutofillOverlayElement.Button, + }); + + expect(buttonPortSpy.disconnect).toHaveBeenCalled(); + expect(overlayBackground["overlayButtonPort"]).toBeNull(); + }); + + it("disconnects the list element port", () => { + sendMockExtensionMessage({ + command: "autofillOverlayElementClosed", + overlayElement: AutofillOverlayElement.List, + }); + + expect(listPortSpy.disconnect).toHaveBeenCalled(); + expect(overlayBackground["overlayListPort"]).toBeNull(); + }); + }); + + describe("autofillOverlayAddNewVaultItem message handler", () => { + let sender: chrome.runtime.MessageSender; + beforeEach(() => { + sender = mock({ tab: { id: 1 } }); + jest + .spyOn(overlayBackground["cipherService"], "setAddEditCipherInfo") + .mockImplementation(); + jest.spyOn(overlayBackground as any, "openAddEditVaultItemPopout").mockImplementation(); + }); + + it("will not open the add edit popout window if the message does not have a login cipher provided", () => { + sendMockExtensionMessage({ command: "autofillOverlayAddNewVaultItem" }, sender); + + expect(overlayBackground["cipherService"].setAddEditCipherInfo).not.toHaveBeenCalled(); + expect(overlayBackground["openAddEditVaultItemPopout"]).not.toHaveBeenCalled(); + }); + + it("will open the add edit popout window after creating a new cipher", async () => { + jest.spyOn(BrowserApi, "sendMessage"); + + sendMockExtensionMessage( + { + command: "autofillOverlayAddNewVaultItem", + login: { + uri: "https://tacos.com", + hostname: "", + username: "username", + password: "password", + }, + }, + sender, + ); + await flushPromises(); + + expect(overlayBackground["cipherService"].setAddEditCipherInfo).toHaveBeenCalled(); + expect(BrowserApi.sendMessage).toHaveBeenCalledWith( + "inlineAutofillMenuRefreshAddEditCipher", + ); + expect(overlayBackground["openAddEditVaultItemPopout"]).toHaveBeenCalled(); + }); + }); + + describe("getAutofillOverlayVisibility message handler", () => { + beforeEach(() => { + jest + .spyOn(overlayBackground as any, "getOverlayVisibility") + .mockResolvedValue(AutofillOverlayVisibility.OnFieldFocus); + }); + + it("will set the overlayVisibility property", async () => { + sendMockExtensionMessage({ command: "getAutofillOverlayVisibility" }); + await flushPromises(); + + expect(await overlayBackground["getOverlayVisibility"]()).toBe( + AutofillOverlayVisibility.OnFieldFocus, + ); + }); + + it("returns the overlayVisibility property", async () => { + const sendMessageSpy = jest.fn(); + + sendMockExtensionMessage( + { command: "getAutofillOverlayVisibility" }, + undefined, + sendMessageSpy, + ); + await flushPromises(); + + expect(sendMessageSpy).toHaveBeenCalledWith(AutofillOverlayVisibility.OnFieldFocus); + }); + }); + + describe("checkAutofillOverlayFocused message handler", () => { + beforeEach(async () => { + await initOverlayElementPorts(); + }); + + it("will check if the overlay list is focused if the list port is open", () => { + sendMockExtensionMessage({ command: "checkAutofillOverlayFocused" }); + + expect(listPortSpy.postMessage).toHaveBeenCalledWith({ + command: "checkAutofillOverlayListFocused", + }); + expect(buttonPortSpy.postMessage).not.toHaveBeenCalledWith({ + command: "checkAutofillOverlayButtonFocused", + }); + }); + + it("will check if the overlay button is focused if the list port is not open", () => { + overlayBackground["overlayListPort"] = undefined; + + sendMockExtensionMessage({ command: "checkAutofillOverlayFocused" }); + + expect(buttonPortSpy.postMessage).toHaveBeenCalledWith({ + command: "checkAutofillOverlayButtonFocused", + }); + expect(listPortSpy.postMessage).not.toHaveBeenCalledWith({ + command: "checkAutofillOverlayListFocused", + }); + }); + }); + + describe("focusAutofillOverlayList message handler", () => { + it("will send a `focusOverlayList` message to the overlay list port", async () => { + await initOverlayElementPorts({ initList: true, initButton: false }); + + sendMockExtensionMessage({ command: "focusAutofillOverlayList" }); + + expect(listPortSpy.postMessage).toHaveBeenCalledWith({ command: "focusOverlayList" }); + }); + }); + + describe("updateAutofillOverlayPosition message handler", () => { + beforeEach(async () => { + await overlayBackground["handlePortOnConnect"]( + createPortSpyMock(AutofillOverlayPort.List), + ); + listPortSpy = overlayBackground["overlayListPort"]; + + await overlayBackground["handlePortOnConnect"]( + createPortSpyMock(AutofillOverlayPort.Button), + ); + buttonPortSpy = overlayBackground["overlayButtonPort"]; + }); + + it("ignores updating the position if the overlay element type is not provided", () => { + sendMockExtensionMessage({ command: "updateAutofillOverlayPosition" }); + + expect(listPortSpy.postMessage).not.toHaveBeenCalledWith({ + command: "updateIframePosition", + styles: expect.anything(), + }); + expect(buttonPortSpy.postMessage).not.toHaveBeenCalledWith({ + command: "updateIframePosition", + styles: expect.anything(), + }); + }); + + it("skips updating the position if the most recently focused field is different than the message sender", () => { + const sender = mock({ tab: { id: 1 } }); + const focusedFieldData = createFocusedFieldDataMock({ tabId: 2 }); + sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); + + sendMockExtensionMessage({ command: "updateAutofillOverlayPosition" }, sender); + + expect(listPortSpy.postMessage).not.toHaveBeenCalledWith({ + command: "updateIframePosition", + styles: expect.anything(), + }); + expect(buttonPortSpy.postMessage).not.toHaveBeenCalledWith({ + command: "updateIframePosition", + styles: expect.anything(), + }); + }); + + it("updates the overlay button's position", () => { + const focusedFieldData = createFocusedFieldDataMock(); + sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); + + sendMockExtensionMessage({ + command: "updateAutofillOverlayPosition", + overlayElement: AutofillOverlayElement.Button, + }); + + expect(buttonPortSpy.postMessage).toHaveBeenCalledWith({ + command: "updateIframePosition", + styles: { height: "2px", left: "4px", top: "2px", width: "2px" }, + }); + }); + + it("modifies the overlay button's height for medium sized input elements", () => { + const focusedFieldData = createFocusedFieldDataMock({ + focusedFieldRects: { top: 1, left: 2, height: 35, width: 4 }, + }); + sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); + + sendMockExtensionMessage({ + command: "updateAutofillOverlayPosition", + overlayElement: AutofillOverlayElement.Button, + }); + + expect(buttonPortSpy.postMessage).toHaveBeenCalledWith({ + command: "updateIframePosition", + styles: { height: "20px", left: "-22px", top: "8px", width: "20px" }, + }); + }); + + it("modifies the overlay button's height for large sized input elements", () => { + const focusedFieldData = createFocusedFieldDataMock({ + focusedFieldRects: { top: 1, left: 2, height: 50, width: 4 }, + }); + sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); + + sendMockExtensionMessage({ + command: "updateAutofillOverlayPosition", + overlayElement: AutofillOverlayElement.Button, + }); + + expect(buttonPortSpy.postMessage).toHaveBeenCalledWith({ + command: "updateIframePosition", + styles: { height: "27px", left: "-32px", top: "13px", width: "27px" }, + }); + }); + + it("takes into account the right padding of the focused field in positioning the button if the right padding of the field is larger than the left padding", () => { + const focusedFieldData = createFocusedFieldDataMock({ + focusedFieldStyles: { paddingRight: "20px", paddingLeft: "6px" }, + }); + sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); + + sendMockExtensionMessage({ + command: "updateAutofillOverlayPosition", + overlayElement: AutofillOverlayElement.Button, + }); + + expect(buttonPortSpy.postMessage).toHaveBeenCalledWith({ + command: "updateIframePosition", + styles: { height: "2px", left: "-18px", top: "2px", width: "2px" }, + }); + }); + + it("will post a message to the overlay list facilitating an update of the list's position", () => { + const sender = mock({ tab: { id: 1 } }); + const focusedFieldData = createFocusedFieldDataMock(); + sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); + + overlayBackground["updateOverlayPosition"]( + { overlayElement: AutofillOverlayElement.List }, + sender, + ); + sendMockExtensionMessage({ + command: "updateAutofillOverlayPosition", + overlayElement: AutofillOverlayElement.List, + }); + + expect(listPortSpy.postMessage).toHaveBeenCalledWith({ + command: "updateIframePosition", + styles: { left: "2px", top: "4px", width: "4px" }, + }); + }); + }); + + describe("updateOverlayHidden", () => { + beforeEach(async () => { + await initOverlayElementPorts(); + }); + + it("returns early if the display value is not provided", () => { + const message = { + command: "updateAutofillOverlayHidden", + }; + + sendMockExtensionMessage(message); + + expect(buttonPortSpy.postMessage).not.toHaveBeenCalledWith(message); + expect(listPortSpy.postMessage).not.toHaveBeenCalledWith(message); + }); + + it("posts a message to the overlay button and list with the display value", () => { + const message = { command: "updateAutofillOverlayHidden", display: "none" }; + + sendMockExtensionMessage(message); + + expect(overlayBackground["overlayButtonPort"].postMessage).toHaveBeenCalledWith({ + command: "updateOverlayHidden", + styles: { + display: message.display, + }, + }); + expect(overlayBackground["overlayListPort"].postMessage).toHaveBeenCalledWith({ + command: "updateOverlayHidden", + styles: { + display: message.display, + }, + }); + }); + }); + + describe("collectPageDetailsResponse message handler", () => { + let sender: chrome.runtime.MessageSender; + const pageDetails1 = createAutofillPageDetailsMock({ + login: { username: "username1", password: "password1" }, + }); + const pageDetails2 = createAutofillPageDetailsMock({ + login: { username: "username2", password: "password2" }, + }); + + beforeEach(() => { + sender = mock({ tab: { id: 1 } }); + }); + + it("stores the page details provided by the message by the tab id of the sender", () => { + sendMockExtensionMessage( + { command: "collectPageDetailsResponse", details: pageDetails1 }, + sender, + ); + + expect(overlayBackground["pageDetailsForTab"][sender.tab.id]).toStrictEqual( + new Map([ + [sender.frameId, { frameId: sender.frameId, tab: sender.tab, details: pageDetails1 }], + ]), + ); + }); + + it("updates the page details for a tab that already has a set of page details stored ", () => { + const secondFrameSender = mock({ + tab: { id: 1 }, + frameId: 3, + }); + overlayBackground["pageDetailsForTab"][sender.tab.id] = new Map([ + [sender.frameId, { frameId: sender.frameId, tab: sender.tab, details: pageDetails1 }], + ]); + + sendMockExtensionMessage( + { command: "collectPageDetailsResponse", details: pageDetails2 }, + secondFrameSender, + ); + + expect(overlayBackground["pageDetailsForTab"][sender.tab.id]).toStrictEqual( + new Map([ + [sender.frameId, { frameId: sender.frameId, tab: sender.tab, details: pageDetails1 }], + [ + secondFrameSender.frameId, + { + frameId: secondFrameSender.frameId, + tab: secondFrameSender.tab, + details: pageDetails2, + }, + ], + ]), + ); + }); + }); + + describe("unlockCompleted message handler", () => { + let getAuthStatusSpy: jest.SpyInstance; + + beforeEach(() => { + overlayBackground["userAuthStatus"] = AuthenticationStatus.LoggedOut; + jest.spyOn(BrowserApi, "tabSendMessageData"); + getAuthStatusSpy = jest + .spyOn(overlayBackground as any, "getAuthStatus") + .mockImplementation(() => { + overlayBackground["userAuthStatus"] = AuthenticationStatus.Unlocked; + return Promise.resolve(AuthenticationStatus.Unlocked); + }); + }); + + it("updates the user's auth status but does not open the overlay", async () => { + const message = { + command: "unlockCompleted", + data: { + commandToRetry: { message: { command: "" } }, + }, + }; + + sendMockExtensionMessage(message); + await flushPromises(); + + expect(getAuthStatusSpy).toHaveBeenCalled(); + expect(BrowserApi.tabSendMessageData).not.toHaveBeenCalled(); + }); + + it("updates user's auth status and opens the overlay if a follow up command is provided", async () => { + const sender = mock({ tab: { id: 1 } }); + const message = { + command: "unlockCompleted", + data: { + commandToRetry: { message: { command: "openAutofillOverlay" } }, + }, + }; + jest.spyOn(BrowserApi, "getTabFromCurrentWindowId").mockResolvedValueOnce(sender.tab); + + sendMockExtensionMessage(message); + await flushPromises(); + + expect(getAuthStatusSpy).toHaveBeenCalled(); + expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith( + sender.tab, + "openAutofillOverlay", + { + isFocusingFieldElement: true, + isOpeningFullOverlay: false, + authStatus: AuthenticationStatus.Unlocked, + }, + ); + }); + }); + + describe("extension messages that trigger an update of the inline menu ciphers", () => { + const extensionMessages = [ + "addedCipher", + "addEditCipherSubmitted", + "editedCipher", + "deletedCipher", + ]; + + beforeEach(() => { + jest.spyOn(overlayBackground, "updateOverlayCiphers").mockImplementation(); + }); + + extensionMessages.forEach((message) => { + it(`triggers an update of the overlay ciphers when the ${message} message is received`, () => { + sendMockExtensionMessage({ command: message }); + expect(overlayBackground.updateOverlayCiphers).toHaveBeenCalled(); + }); + }); + }); + }); + }); + + describe("handlePortOnConnect", () => { + beforeEach(() => { + jest.spyOn(overlayBackground as any, "updateOverlayPosition").mockImplementation(); + jest.spyOn(overlayBackground as any, "getAuthStatus").mockImplementation(); + jest.spyOn(overlayBackground as any, "getTranslations").mockImplementation(); + jest.spyOn(overlayBackground as any, "getOverlayCipherData").mockImplementation(); + }); + + it("skips setting up the overlay port if the port connection is not for an overlay element", async () => { + const port = createPortSpyMock("not-an-overlay-element"); + + await overlayBackground["handlePortOnConnect"](port); + + expect(port.onMessage.addListener).not.toHaveBeenCalled(); + expect(port.postMessage).not.toHaveBeenCalled(); + }); + + it("sets up the overlay list port if the port connection is for the overlay list", async () => { + await initOverlayElementPorts({ initList: true, initButton: false }); + await flushPromises(); + + expect(overlayBackground["overlayButtonPort"]).toBeUndefined(); + expect(listPortSpy.onMessage.addListener).toHaveBeenCalled(); + expect(listPortSpy.postMessage).toHaveBeenCalled(); + expect(overlayBackground["getAuthStatus"]).toHaveBeenCalled(); + expect(chrome.runtime.getURL).toHaveBeenCalledWith("overlay/list.css"); + expect(overlayBackground["getTranslations"]).toHaveBeenCalled(); + expect(overlayBackground["getOverlayCipherData"]).toHaveBeenCalled(); + expect(overlayBackground["updateOverlayPosition"]).toHaveBeenCalledWith( + { overlayElement: AutofillOverlayElement.List }, + listPortSpy.sender, + ); + }); + + it("sets up the overlay button port if the port connection is for the overlay button", async () => { + await initOverlayElementPorts({ initList: false, initButton: true }); + await flushPromises(); + + expect(overlayBackground["overlayListPort"]).toBeUndefined(); + expect(buttonPortSpy.onMessage.addListener).toHaveBeenCalled(); + expect(buttonPortSpy.postMessage).toHaveBeenCalled(); + expect(overlayBackground["getAuthStatus"]).toHaveBeenCalled(); + expect(chrome.runtime.getURL).toHaveBeenCalledWith("overlay/button.css"); + expect(overlayBackground["getTranslations"]).toHaveBeenCalled(); + expect(overlayBackground["updateOverlayPosition"]).toHaveBeenCalledWith( + { overlayElement: AutofillOverlayElement.Button }, + buttonPortSpy.sender, + ); + }); + + it("stores an existing overlay port so that it can be disconnected at a later time", async () => { + overlayBackground["overlayButtonPort"] = mock(); + + await initOverlayElementPorts({ initList: false, initButton: true }); + await flushPromises(); + + expect(overlayBackground["expiredPorts"].length).toBe(1); + }); + + it("gets the system theme", async () => { + themeStateService.selectedTheme$ = of(ThemeType.System); + + await initOverlayElementPorts({ initList: true, initButton: false }); + await flushPromises(); + + expect(listPortSpy.postMessage).toHaveBeenCalledWith( + expect.objectContaining({ theme: ThemeType.System }), + ); + }); + }); + + describe("handleOverlayElementPortMessage", () => { + beforeEach(async () => { + await initOverlayElementPorts(); + overlayBackground["userAuthStatus"] = AuthenticationStatus.Unlocked; + }); + + it("ignores port messages that do not contain a handler", () => { + jest.spyOn(overlayBackground as any, "checkOverlayButtonFocused").mockImplementation(); + + sendPortMessage(buttonPortSpy, { command: "checkAutofillOverlayButtonFocused" }); + + expect(overlayBackground["checkOverlayButtonFocused"]).not.toHaveBeenCalled(); + }); + + describe("overlay button message handlers", () => { + it("unlocks the vault if the user auth status is not unlocked", () => { + overlayBackground["userAuthStatus"] = AuthenticationStatus.LoggedOut; + jest.spyOn(overlayBackground as any, "unlockVault").mockImplementation(); + + sendPortMessage(buttonPortSpy, { command: "overlayButtonClicked" }); + + expect(overlayBackground["unlockVault"]).toHaveBeenCalled(); + }); + + it("opens the autofill overlay if the auth status is unlocked", () => { + jest.spyOn(overlayBackground as any, "openOverlay").mockImplementation(); + + sendPortMessage(buttonPortSpy, { command: "overlayButtonClicked" }); + + expect(overlayBackground["openOverlay"]).toHaveBeenCalled(); + }); + + describe("closeAutofillOverlay", () => { + it("sends a `closeOverlay` message to the sender tab", () => { + jest.spyOn(BrowserApi, "tabSendMessageData"); + + sendPortMessage(buttonPortSpy, { command: "closeAutofillOverlay" }); + + expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith( + buttonPortSpy.sender.tab, + "closeAutofillOverlay", + { forceCloseOverlay: false }, + ); + }); + }); + + describe("forceCloseAutofillOverlay", () => { + it("sends a `closeOverlay` message to the sender tab with a `forceCloseOverlay` flag of `true` set", () => { + jest.spyOn(BrowserApi, "tabSendMessageData"); + + sendPortMessage(buttonPortSpy, { command: "forceCloseAutofillOverlay" }); + + expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith( + buttonPortSpy.sender.tab, + "closeAutofillOverlay", + { forceCloseOverlay: true }, + ); + }); + }); + + describe("overlayPageBlurred", () => { + it("checks if the overlay list is focused", () => { + jest.spyOn(overlayBackground as any, "checkOverlayListFocused"); + + sendPortMessage(buttonPortSpy, { command: "overlayPageBlurred" }); + + expect(overlayBackground["checkOverlayListFocused"]).toHaveBeenCalled(); + }); + }); + + describe("redirectOverlayFocusOut", () => { + beforeEach(() => { + jest.spyOn(BrowserApi, "tabSendMessageData"); + }); + + it("ignores the redirect message if the direction is not provided", () => { + sendPortMessage(buttonPortSpy, { command: "redirectOverlayFocusOut" }); + + expect(BrowserApi.tabSendMessageData).not.toHaveBeenCalled(); + }); + + it("sends the redirect message if the direction is provided", () => { + sendPortMessage(buttonPortSpy, { + command: "redirectOverlayFocusOut", + direction: RedirectFocusDirection.Next, + }); + + expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith( + buttonPortSpy.sender.tab, + "redirectOverlayFocusOut", + { direction: RedirectFocusDirection.Next }, + ); + }); + }); + }); + + describe("overlay list message handlers", () => { + describe("checkAutofillOverlayButtonFocused", () => { + it("checks on the focus state of the overlay button", () => { + jest.spyOn(overlayBackground as any, "checkOverlayButtonFocused").mockImplementation(); + + sendPortMessage(listPortSpy, { command: "checkAutofillOverlayButtonFocused" }); + + expect(overlayBackground["checkOverlayButtonFocused"]).toHaveBeenCalled(); + }); + }); + + describe("forceCloseAutofillOverlay", () => { + it("sends a `closeOverlay` message to the sender tab with a `forceCloseOverlay` flag of `true` set", () => { + jest.spyOn(BrowserApi, "tabSendMessageData"); + + sendPortMessage(listPortSpy, { command: "forceCloseAutofillOverlay" }); + + expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith( + listPortSpy.sender.tab, + "closeAutofillOverlay", + { forceCloseOverlay: true }, + ); + }); + }); + + describe("overlayPageBlurred", () => { + it("checks on the focus state of the overlay button", () => { + jest.spyOn(overlayBackground as any, "checkOverlayButtonFocused").mockImplementation(); + + sendPortMessage(listPortSpy, { command: "overlayPageBlurred" }); + + expect(overlayBackground["checkOverlayButtonFocused"]).toHaveBeenCalled(); + }); + }); + + describe("unlockVault", () => { + it("closes the autofill overlay and opens the unlock popout", async () => { + jest.spyOn(overlayBackground as any, "closeOverlay").mockImplementation(); + jest.spyOn(overlayBackground as any, "openUnlockPopout").mockImplementation(); + jest.spyOn(BrowserApi, "tabSendMessageData").mockImplementation(); + + sendPortMessage(listPortSpy, { command: "unlockVault" }); + await flushPromises(); + + expect(overlayBackground["closeOverlay"]).toHaveBeenCalledWith(listPortSpy); + expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith( + listPortSpy.sender.tab, + "addToLockedVaultPendingNotifications", + { + commandToRetry: { + message: { command: "openAutofillOverlay" }, + sender: listPortSpy.sender, + }, + target: "overlay.background", + }, + ); + expect(overlayBackground["openUnlockPopout"]).toHaveBeenCalledWith( + listPortSpy.sender.tab, + true, + ); + }); + }); + + describe("fillSelectedListItem", () => { + let getLoginCiphersSpy: jest.SpyInstance; + let isPasswordRepromptRequiredSpy: jest.SpyInstance; + let doAutoFillSpy: jest.SpyInstance; + let sender: chrome.runtime.MessageSender; + const pageDetails = createAutofillPageDetailsMock({ + login: { username: "username1", password: "password1" }, + }); + + beforeEach(() => { + getLoginCiphersSpy = jest.spyOn(overlayBackground["overlayLoginCiphers"], "get"); + isPasswordRepromptRequiredSpy = jest.spyOn( + overlayBackground["autofillService"], + "isPasswordRepromptRequired", + ); + doAutoFillSpy = jest.spyOn(overlayBackground["autofillService"], "doAutoFill"); + sender = mock({ tab: { id: 1 } }); + }); + + it("ignores the fill request if the overlay cipher id is not provided", async () => { + sendPortMessage(listPortSpy, { command: "fillSelectedListItem" }); + await flushPromises(); + + expect(getLoginCiphersSpy).not.toHaveBeenCalled(); + expect(isPasswordRepromptRequiredSpy).not.toHaveBeenCalled(); + expect(doAutoFillSpy).not.toHaveBeenCalled(); + }); + + it("ignores the fill request if the tab does not contain any identified page details", async () => { + sendPortMessage(listPortSpy, { + command: "fillSelectedListItem", + overlayCipherId: "overlay-cipher-1", + }); + await flushPromises(); + + expect(getLoginCiphersSpy).not.toHaveBeenCalled(); + expect(isPasswordRepromptRequiredSpy).not.toHaveBeenCalled(); + expect(doAutoFillSpy).not.toHaveBeenCalled(); + }); + + it("ignores the fill request if a master password reprompt is required", async () => { + const cipher = mock({ + reprompt: CipherRepromptType.Password, + type: CipherType.Login, + }); + overlayBackground["overlayLoginCiphers"] = new Map([["overlay-cipher-1", cipher]]); + overlayBackground["pageDetailsForTab"][sender.tab.id] = new Map([ + [sender.frameId, { frameId: sender.frameId, tab: sender.tab, details: pageDetails }], + ]); + getLoginCiphersSpy = jest.spyOn(overlayBackground["overlayLoginCiphers"], "get"); + isPasswordRepromptRequiredSpy.mockResolvedValue(true); + + sendPortMessage(listPortSpy, { + command: "fillSelectedListItem", + overlayCipherId: "overlay-cipher-1", + }); + await flushPromises(); + + expect(getLoginCiphersSpy).toHaveBeenCalled(); + expect(isPasswordRepromptRequiredSpy).toHaveBeenCalledWith( + cipher, + listPortSpy.sender.tab, + ); + expect(doAutoFillSpy).not.toHaveBeenCalled(); + }); + + it("auto-fills the selected cipher and move it to the top of the front of the ciphers map", async () => { + const cipher1 = mock({ id: "overlay-cipher-1" }); + const cipher2 = mock({ id: "overlay-cipher-2" }); + const cipher3 = mock({ id: "overlay-cipher-3" }); + overlayBackground["overlayLoginCiphers"] = new Map([ + ["overlay-cipher-1", cipher1], + ["overlay-cipher-2", cipher2], + ["overlay-cipher-3", cipher3], + ]); + const pageDetailsForTab = { + frameId: sender.frameId, + tab: sender.tab, + details: pageDetails, + }; + overlayBackground["pageDetailsForTab"][sender.tab.id] = new Map([ + [sender.frameId, pageDetailsForTab], + ]); + isPasswordRepromptRequiredSpy.mockResolvedValue(false); + + sendPortMessage(listPortSpy, { + command: "fillSelectedListItem", + overlayCipherId: "overlay-cipher-2", + }); + await flushPromises(); + + expect(isPasswordRepromptRequiredSpy).toHaveBeenCalledWith( + cipher2, + listPortSpy.sender.tab, + ); + expect(doAutoFillSpy).toHaveBeenCalledWith({ + tab: listPortSpy.sender.tab, + cipher: cipher2, + pageDetails: [pageDetailsForTab], + fillNewPassword: true, + allowTotpAutofill: true, + }); + expect(overlayBackground["overlayLoginCiphers"].entries()).toStrictEqual( + new Map([ + ["overlay-cipher-2", cipher2], + ["overlay-cipher-1", cipher1], + ["overlay-cipher-3", cipher3], + ]).entries(), + ); + }); + + it("copies the cipher's totp code to the clipboard after filling", async () => { + const cipher1 = mock({ id: "overlay-cipher-1" }); + overlayBackground["overlayLoginCiphers"] = new Map([["overlay-cipher-1", cipher1]]); + overlayBackground["pageDetailsForTab"][sender.tab.id] = new Map([ + [sender.frameId, { frameId: sender.frameId, tab: sender.tab, details: pageDetails }], + ]); + isPasswordRepromptRequiredSpy.mockResolvedValue(false); + const copyToClipboardSpy = jest + .spyOn(overlayBackground["platformUtilsService"], "copyToClipboard") + .mockImplementation(); + doAutoFillSpy.mockReturnValueOnce("totp-code"); + + sendPortMessage(listPortSpy, { + command: "fillSelectedListItem", + overlayCipherId: "overlay-cipher-2", + }); + await flushPromises(); + + expect(copyToClipboardSpy).toHaveBeenCalledWith("totp-code"); + }); + }); + + describe("getNewVaultItemDetails", () => { + it("will send an addNewVaultItemFromOverlay message", async () => { + jest.spyOn(BrowserApi, "tabSendMessage"); + + sendPortMessage(listPortSpy, { command: "addNewVaultItem" }); + await flushPromises(); + + expect(BrowserApi.tabSendMessage).toHaveBeenCalledWith(listPortSpy.sender.tab, { + command: "addNewVaultItemFromOverlay", + }); + }); + }); + + describe("viewSelectedCipher", () => { + let openViewVaultItemPopoutSpy: jest.SpyInstance; + + beforeEach(() => { + openViewVaultItemPopoutSpy = jest + .spyOn(overlayBackground as any, "openViewVaultItemPopout") + .mockImplementation(); + }); + + it("returns early if the passed cipher ID does not match one of the overlay login ciphers", async () => { + overlayBackground["overlayLoginCiphers"] = new Map([ + ["overlay-cipher-0", mock({ id: "overlay-cipher-0" })], + ]); + + sendPortMessage(listPortSpy, { + command: "viewSelectedCipher", + overlayCipherId: "overlay-cipher-1", + }); + await flushPromises(); + + expect(openViewVaultItemPopoutSpy).not.toHaveBeenCalled(); + }); + + it("will open the view vault item popout with the selected cipher", async () => { + const cipher = mock({ id: "overlay-cipher-1" }); + overlayBackground["overlayLoginCiphers"] = new Map([ + ["overlay-cipher-0", mock({ id: "overlay-cipher-0" })], + ["overlay-cipher-1", cipher], + ]); + + sendPortMessage(listPortSpy, { + command: "viewSelectedCipher", + overlayCipherId: "overlay-cipher-1", + }); + await flushPromises(); + + expect(overlayBackground["openViewVaultItemPopout"]).toHaveBeenCalledWith( + listPortSpy.sender.tab, + { + cipherId: cipher.id, + action: SHOW_AUTOFILL_BUTTON, + }, + ); + }); + }); + + describe("redirectOverlayFocusOut", () => { + it("redirects focus out of the overlay list", async () => { + const message = { + command: "redirectOverlayFocusOut", + direction: RedirectFocusDirection.Next, + }; + const redirectOverlayFocusOutSpy = jest.spyOn( + overlayBackground as any, + "redirectOverlayFocusOut", + ); + + sendPortMessage(listPortSpy, message); + await flushPromises(); + + expect(redirectOverlayFocusOutSpy).toHaveBeenCalledWith(message, listPortSpy); + }); + }); + }); + }); +}); diff --git a/apps/browser/src/autofill/deprecated/background/overlay.background.deprecated.ts b/apps/browser/src/autofill/deprecated/background/overlay.background.deprecated.ts new file mode 100644 index 00000000000..1a5d49e9e1f --- /dev/null +++ b/apps/browser/src/autofill/deprecated/background/overlay.background.deprecated.ts @@ -0,0 +1,798 @@ +import { firstValueFrom } from "rxjs"; + +import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; +import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; +import { SHOW_AUTOFILL_BUTTON } from "@bitwarden/common/autofill/constants"; +import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; +import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; +import { InlineMenuVisibilitySetting } from "@bitwarden/common/autofill/types"; +import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { CipherType } from "@bitwarden/common/vault/enums"; +import { buildCipherIcon } from "@bitwarden/common/vault/icon/build-cipher-icon"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view"; +import { LoginView } from "@bitwarden/common/vault/models/view/login.view"; + +import { openUnlockPopout } from "../../../auth/popup/utils/auth-popout-window"; +import { BrowserApi } from "../../../platform/browser/browser-api"; +import { + openViewVaultItemPopout, + openAddEditVaultItemPopout, +} from "../../../vault/popup/utils/vault-popout-window"; +import { LockedVaultPendingNotificationsData } from "../../background/abstractions/notification.background"; +import { OverlayBackground as OverlayBackgroundInterface } from "../../background/abstractions/overlay.background"; +import { AutofillOverlayElement, AutofillOverlayPort } from "../../enums/autofill-overlay.enum"; +import { AutofillService, PageDetail } from "../../services/abstractions/autofill.service"; + +import { + FocusedFieldData, + OverlayBackgroundExtensionMessageHandlers, + OverlayButtonPortMessageHandlers, + OverlayCipherData, + OverlayListPortMessageHandlers, + OverlayBackgroundExtensionMessage, + OverlayAddNewItemMessage, + OverlayPortMessage, + WebsiteIconData, +} from "./abstractions/overlay.background.deprecated"; + +class LegacyOverlayBackground implements OverlayBackgroundInterface { + private readonly openUnlockPopout = openUnlockPopout; + private readonly openViewVaultItemPopout = openViewVaultItemPopout; + private readonly openAddEditVaultItemPopout = openAddEditVaultItemPopout; + private overlayLoginCiphers: Map = new Map(); + private pageDetailsForTab: Record< + chrome.runtime.MessageSender["tab"]["id"], + Map + > = {}; + private userAuthStatus: AuthenticationStatus = AuthenticationStatus.LoggedOut; + private overlayButtonPort: chrome.runtime.Port; + private overlayListPort: chrome.runtime.Port; + private expiredPorts: chrome.runtime.Port[] = []; + private focusedFieldData: FocusedFieldData; + private overlayPageTranslations: Record; + private iconsServerUrl: string; + private readonly extensionMessageHandlers: OverlayBackgroundExtensionMessageHandlers = { + openAutofillOverlay: () => this.openOverlay(false), + autofillOverlayElementClosed: ({ message, sender }) => + this.overlayElementClosed(message, sender), + autofillOverlayAddNewVaultItem: ({ message, sender }) => this.addNewVaultItem(message, sender), + getAutofillOverlayVisibility: () => this.getOverlayVisibility(), + checkAutofillOverlayFocused: () => this.checkOverlayFocused(), + focusAutofillOverlayList: () => this.focusOverlayList(), + updateAutofillOverlayPosition: ({ message, sender }) => + this.updateOverlayPosition(message, sender), + updateAutofillOverlayHidden: ({ message }) => this.updateOverlayHidden(message), + updateFocusedFieldData: ({ message, sender }) => this.setFocusedFieldData(message, sender), + collectPageDetailsResponse: ({ message, sender }) => this.storePageDetails(message, sender), + unlockCompleted: ({ message }) => this.unlockCompleted(message), + addedCipher: () => this.updateOverlayCiphers(), + addEditCipherSubmitted: () => this.updateOverlayCiphers(), + editedCipher: () => this.updateOverlayCiphers(), + deletedCipher: () => this.updateOverlayCiphers(), + }; + private readonly overlayButtonPortMessageHandlers: OverlayButtonPortMessageHandlers = { + overlayButtonClicked: ({ port }) => this.handleOverlayButtonClicked(port), + closeAutofillOverlay: ({ port }) => this.closeOverlay(port), + forceCloseAutofillOverlay: ({ port }) => this.closeOverlay(port, true), + overlayPageBlurred: () => this.checkOverlayListFocused(), + redirectOverlayFocusOut: ({ message, port }) => this.redirectOverlayFocusOut(message, port), + }; + private readonly overlayListPortMessageHandlers: OverlayListPortMessageHandlers = { + checkAutofillOverlayButtonFocused: () => this.checkOverlayButtonFocused(), + forceCloseAutofillOverlay: ({ port }) => this.closeOverlay(port, true), + overlayPageBlurred: () => this.checkOverlayButtonFocused(), + unlockVault: ({ port }) => this.unlockVault(port), + fillSelectedListItem: ({ message, port }) => this.fillSelectedOverlayListItem(message, port), + addNewVaultItem: ({ port }) => this.getNewVaultItemDetails(port), + viewSelectedCipher: ({ message, port }) => this.viewSelectedCipher(message, port), + redirectOverlayFocusOut: ({ message, port }) => this.redirectOverlayFocusOut(message, port), + }; + + constructor( + private cipherService: CipherService, + private autofillService: AutofillService, + private authService: AuthService, + private environmentService: EnvironmentService, + private domainSettingsService: DomainSettingsService, + private autofillSettingsService: AutofillSettingsServiceAbstraction, + private i18nService: I18nService, + private platformUtilsService: PlatformUtilsService, + private themeStateService: ThemeStateService, + ) {} + + /** + * Removes cached page details for a tab + * based on the passed tabId. + * + * @param tabId - Used to reference the page details of a specific tab + */ + removePageDetails(tabId: number) { + if (!this.pageDetailsForTab[tabId]) { + return; + } + + this.pageDetailsForTab[tabId].clear(); + delete this.pageDetailsForTab[tabId]; + } + + /** + * Sets up the extension message listeners and gets the settings for the + * overlay's visibility and the user's authentication status. + */ + async init() { + this.setupExtensionMessageListeners(); + const env = await firstValueFrom(this.environmentService.environment$); + this.iconsServerUrl = env.getIconsUrl(); + await this.getOverlayVisibility(); + await this.getAuthStatus(); + } + + /** + * Updates the overlay list's ciphers and sends the updated list to the overlay list iframe. + * Queries all ciphers for the given url, and sorts them by last used. Will not update the + * list of ciphers if the extension is not unlocked. + */ + async updateOverlayCiphers() { + const authStatus = await firstValueFrom(this.authService.activeAccountStatus$); + if (authStatus !== AuthenticationStatus.Unlocked) { + return; + } + + const currentTab = await BrowserApi.getTabFromCurrentWindowId(); + if (!currentTab?.url) { + return; + } + + this.overlayLoginCiphers = new Map(); + const ciphersViews = (await this.cipherService.getAllDecryptedForUrl(currentTab.url)).sort( + (a, b) => this.cipherService.sortCiphersByLastUsedThenName(a, b), + ); + for (let cipherIndex = 0; cipherIndex < ciphersViews.length; cipherIndex++) { + this.overlayLoginCiphers.set(`overlay-cipher-${cipherIndex}`, ciphersViews[cipherIndex]); + } + + const ciphers = await this.getOverlayCipherData(); + this.overlayListPort?.postMessage({ command: "updateOverlayListCiphers", ciphers }); + await BrowserApi.tabSendMessageData(currentTab, "updateIsOverlayCiphersPopulated", { + isOverlayCiphersPopulated: Boolean(ciphers.length), + }); + } + + /** + * Strips out unnecessary data from the ciphers and returns an array of + * objects that contain the cipher data needed for the overlay list. + */ + private async getOverlayCipherData(): Promise { + const showFavicons = await firstValueFrom(this.domainSettingsService.showFavicons$); + const overlayCiphersArray = Array.from(this.overlayLoginCiphers); + const overlayCipherData: OverlayCipherData[] = []; + let loginCipherIcon: WebsiteIconData; + + for (let cipherIndex = 0; cipherIndex < overlayCiphersArray.length; cipherIndex++) { + const [overlayCipherId, cipher] = overlayCiphersArray[cipherIndex]; + if (!loginCipherIcon && cipher.type === CipherType.Login) { + loginCipherIcon = buildCipherIcon(this.iconsServerUrl, cipher, showFavicons); + } + + overlayCipherData.push({ + id: overlayCipherId, + name: cipher.name, + type: cipher.type, + reprompt: cipher.reprompt, + favorite: cipher.favorite, + icon: + cipher.type === CipherType.Login + ? loginCipherIcon + : buildCipherIcon(this.iconsServerUrl, cipher, showFavicons), + login: cipher.type === CipherType.Login ? { username: cipher.login.username } : null, + card: cipher.type === CipherType.Card ? cipher.card.subTitle : null, + }); + } + + return overlayCipherData; + } + + /** + * Handles aggregation of page details for a tab. Stores the page details + * in association with the tabId of the tab that sent the message. + * + * @param message - Message received from the `collectPageDetailsResponse` command + * @param sender - The sender of the message + */ + private storePageDetails( + message: OverlayBackgroundExtensionMessage, + sender: chrome.runtime.MessageSender, + ) { + const pageDetails = { + frameId: sender.frameId, + tab: sender.tab, + details: message.details, + }; + + const pageDetailsMap = this.pageDetailsForTab[sender.tab.id]; + if (!pageDetailsMap) { + this.pageDetailsForTab[sender.tab.id] = new Map([[sender.frameId, pageDetails]]); + return; + } + + pageDetailsMap.set(sender.frameId, pageDetails); + } + + /** + * Triggers autofill for the selected cipher in the overlay list. Also places + * the selected cipher at the top of the list of ciphers. + * + * @param overlayCipherId - Cipher ID corresponding to the overlayLoginCiphers map. Does not correspond to the actual cipher's ID. + * @param sender - The sender of the port message + */ + private async fillSelectedOverlayListItem( + { overlayCipherId }: OverlayPortMessage, + { sender }: chrome.runtime.Port, + ) { + const pageDetails = this.pageDetailsForTab[sender.tab.id]; + if (!overlayCipherId || !pageDetails?.size) { + return; + } + + const cipher = this.overlayLoginCiphers.get(overlayCipherId); + + if (await this.autofillService.isPasswordRepromptRequired(cipher, sender.tab)) { + return; + } + const totpCode = await this.autofillService.doAutoFill({ + tab: sender.tab, + cipher: cipher, + pageDetails: Array.from(pageDetails.values()), + fillNewPassword: true, + allowTotpAutofill: true, + }); + + if (totpCode) { + this.platformUtilsService.copyToClipboard(totpCode); + } + + this.overlayLoginCiphers = new Map([[overlayCipherId, cipher], ...this.overlayLoginCiphers]); + } + + /** + * Checks if the overlay is focused. Will check the overlay list + * if it is open, otherwise it will check the overlay button. + */ + private checkOverlayFocused() { + if (this.overlayListPort) { + this.checkOverlayListFocused(); + + return; + } + + this.checkOverlayButtonFocused(); + } + + /** + * Posts a message to the overlay button iframe to check if it is focused. + */ + private checkOverlayButtonFocused() { + this.overlayButtonPort?.postMessage({ command: "checkAutofillOverlayButtonFocused" }); + } + + /** + * Posts a message to the overlay list iframe to check if it is focused. + */ + private checkOverlayListFocused() { + this.overlayListPort?.postMessage({ command: "checkAutofillOverlayListFocused" }); + } + + /** + * Sends a message to the sender tab to close the autofill overlay. + * + * @param sender - The sender of the port message + * @param forceCloseOverlay - Identifies whether the overlay should be force closed + */ + private closeOverlay({ sender }: chrome.runtime.Port, forceCloseOverlay = false) { + // 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 + BrowserApi.tabSendMessageData(sender.tab, "closeAutofillOverlay", { forceCloseOverlay }); + } + + /** + * Handles cleanup when an overlay element is closed. Disconnects + * the list and button ports and sets them to null. + * + * @param overlayElement - The overlay element that was closed, either the list or button + * @param sender - The sender of the port message + */ + private overlayElementClosed( + { overlayElement }: OverlayBackgroundExtensionMessage, + sender: chrome.runtime.MessageSender, + ) { + if (sender.tab.id !== this.focusedFieldData?.tabId) { + this.expiredPorts.forEach((port) => port.disconnect()); + this.expiredPorts = []; + return; + } + + if (overlayElement === AutofillOverlayElement.Button) { + this.overlayButtonPort?.disconnect(); + this.overlayButtonPort = null; + + return; + } + + this.overlayListPort?.disconnect(); + this.overlayListPort = null; + } + + /** + * Updates the position of either the overlay list or button. The position + * is based on the focused field's position and dimensions. + * + * @param overlayElement - The overlay element to update, either the list or button + * @param sender - The sender of the port message + */ + private updateOverlayPosition( + { overlayElement }: { overlayElement?: string }, + sender: chrome.runtime.MessageSender, + ) { + if (!overlayElement || sender.tab.id !== this.focusedFieldData?.tabId) { + return; + } + + if (overlayElement === AutofillOverlayElement.Button) { + this.overlayButtonPort?.postMessage({ + command: "updateIframePosition", + styles: this.getOverlayButtonPosition(), + }); + + return; + } + + this.overlayListPort?.postMessage({ + command: "updateIframePosition", + styles: this.getOverlayListPosition(), + }); + } + + /** + * Gets the position of the focused field and calculates the position + * of the overlay button based on the focused field's position and dimensions. + */ + private getOverlayButtonPosition() { + if (!this.focusedFieldData) { + return; + } + + const { top, left, width, height } = this.focusedFieldData.focusedFieldRects; + const { paddingRight, paddingLeft } = this.focusedFieldData.focusedFieldStyles; + let elementOffset = height * 0.37; + if (height >= 35) { + elementOffset = height >= 50 ? height * 0.47 : height * 0.42; + } + + const elementHeight = height - elementOffset; + const elementTopPosition = top + elementOffset / 2; + let elementLeftPosition = left + width - height + elementOffset / 2; + + const fieldPaddingRight = parseInt(paddingRight, 10); + const fieldPaddingLeft = parseInt(paddingLeft, 10); + if (fieldPaddingRight > fieldPaddingLeft) { + elementLeftPosition = left + width - height - (fieldPaddingRight - elementOffset + 2); + } + + return { + top: `${Math.round(elementTopPosition)}px`, + left: `${Math.round(elementLeftPosition)}px`, + height: `${Math.round(elementHeight)}px`, + width: `${Math.round(elementHeight)}px`, + }; + } + + /** + * Gets the position of the focused field and calculates the position + * of the overlay list based on the focused field's position and dimensions. + */ + private getOverlayListPosition() { + if (!this.focusedFieldData) { + return; + } + + const { top, left, width, height } = this.focusedFieldData.focusedFieldRects; + return { + width: `${Math.round(width)}px`, + top: `${Math.round(top + height)}px`, + left: `${Math.round(left)}px`, + }; + } + + /** + * Sets the focused field data to the data passed in the extension message. + * + * @param focusedFieldData - Contains the rects and styles of the focused field. + * @param sender - The sender of the extension message + */ + private setFocusedFieldData( + { focusedFieldData }: OverlayBackgroundExtensionMessage, + sender: chrome.runtime.MessageSender, + ) { + this.focusedFieldData = { ...focusedFieldData, tabId: sender.tab.id }; + } + + /** + * Updates the overlay's visibility based on the display property passed in the extension message. + * + * @param display - The display property of the overlay, either "block" or "none" + */ + private updateOverlayHidden({ display }: OverlayBackgroundExtensionMessage) { + if (!display) { + return; + } + + const portMessage = { command: "updateOverlayHidden", styles: { display } }; + + this.overlayButtonPort?.postMessage(portMessage); + this.overlayListPort?.postMessage(portMessage); + } + + /** + * Sends a message to the currently active tab to open the autofill overlay. + * + * @param isFocusingFieldElement - Identifies whether the field element should be focused when the overlay is opened + * @param isOpeningFullOverlay - Identifies whether the full overlay should be forced open regardless of other states + */ + private async openOverlay(isFocusingFieldElement = false, isOpeningFullOverlay = false) { + const currentTab = await BrowserApi.getTabFromCurrentWindowId(); + + await BrowserApi.tabSendMessageData(currentTab, "openAutofillOverlay", { + isFocusingFieldElement, + isOpeningFullOverlay, + authStatus: await this.getAuthStatus(), + }); + } + + /** + * Gets the overlay's visibility setting from the settings service. + */ + private async getOverlayVisibility(): Promise { + return await firstValueFrom(this.autofillSettingsService.inlineMenuVisibility$); + } + + /** + * Gets the user's authentication status from the auth service. If the user's + * authentication status has changed, the overlay button's authentication status + * will be updated and the overlay list's ciphers will be updated. + */ + private async getAuthStatus() { + const formerAuthStatus = this.userAuthStatus; + this.userAuthStatus = await this.authService.getAuthStatus(); + + if ( + this.userAuthStatus !== formerAuthStatus && + this.userAuthStatus === AuthenticationStatus.Unlocked + ) { + this.updateOverlayButtonAuthStatus(); + await this.updateOverlayCiphers(); + } + + return this.userAuthStatus; + } + + /** + * Sends a message to the overlay button to update its authentication status. + */ + private updateOverlayButtonAuthStatus() { + this.overlayButtonPort?.postMessage({ + command: "updateOverlayButtonAuthStatus", + authStatus: this.userAuthStatus, + }); + } + + /** + * Handles the overlay button being clicked. If the user is not authenticated, + * the vault will be unlocked. If the user is authenticated, the overlay will + * be opened. + * + * @param port - The port of the overlay button + */ + private handleOverlayButtonClicked(port: chrome.runtime.Port) { + if (this.userAuthStatus !== AuthenticationStatus.Unlocked) { + // 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.unlockVault(port); + return; + } + + // 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.openOverlay(false, true); + } + + /** + * Facilitates opening the unlock popout window. + * + * @param port - The port of the overlay list + */ + private async unlockVault(port: chrome.runtime.Port) { + const { sender } = port; + + this.closeOverlay(port); + const retryMessage: LockedVaultPendingNotificationsData = { + commandToRetry: { message: { command: "openAutofillOverlay" }, sender }, + target: "overlay.background", + }; + await BrowserApi.tabSendMessageData( + sender.tab, + "addToLockedVaultPendingNotifications", + retryMessage, + ); + await this.openUnlockPopout(sender.tab, true); + } + + /** + * Triggers the opening of a vault item popout window associated + * with the passed cipher ID. + * @param overlayCipherId - Cipher ID corresponding to the overlayLoginCiphers map. Does not correspond to the actual cipher's ID. + * @param sender - The sender of the port message + */ + private async viewSelectedCipher( + { overlayCipherId }: OverlayPortMessage, + { sender }: chrome.runtime.Port, + ) { + const cipher = this.overlayLoginCiphers.get(overlayCipherId); + if (!cipher) { + return; + } + + await this.openViewVaultItemPopout(sender.tab, { + cipherId: cipher.id, + action: SHOW_AUTOFILL_BUTTON, + }); + } + + /** + * Facilitates redirecting focus to the overlay list. + */ + private focusOverlayList() { + this.overlayListPort?.postMessage({ command: "focusOverlayList" }); + } + + /** + * Updates the authentication status for the user and opens the overlay if + * a followup command is present in the message. + * + * @param message - Extension message received from the `unlockCompleted` command + */ + private async unlockCompleted(message: OverlayBackgroundExtensionMessage) { + await this.getAuthStatus(); + + if (message.data?.commandToRetry?.message?.command === "openAutofillOverlay") { + await this.openOverlay(true); + } + } + + /** + * Gets the translations for the overlay page. + */ + private getTranslations() { + if (!this.overlayPageTranslations) { + this.overlayPageTranslations = { + locale: BrowserApi.getUILanguage(), + opensInANewWindow: this.i18nService.translate("opensInANewWindow"), + buttonPageTitle: this.i18nService.translate("bitwardenOverlayButton"), + toggleBitwardenVaultOverlay: this.i18nService.translate("toggleBitwardenVaultOverlay"), + listPageTitle: this.i18nService.translate("bitwardenVault"), + unlockYourAccount: this.i18nService.translate("unlockYourAccountToViewMatchingLogins"), + unlockAccount: this.i18nService.translate("unlockAccount"), + fillCredentialsFor: this.i18nService.translate("fillCredentialsFor"), + partialUsername: this.i18nService.translate("partialUsername"), + view: this.i18nService.translate("view"), + noItemsToShow: this.i18nService.translate("noItemsToShow"), + newItem: this.i18nService.translate("newItem"), + addNewVaultItem: this.i18nService.translate("addNewVaultItem"), + }; + } + + return this.overlayPageTranslations; + } + + /** + * Facilitates redirecting focus out of one of the + * overlay elements to elements on the page. + * + * @param direction - The direction to redirect focus to (either "next", "previous" or "current) + * @param sender - The sender of the port message + */ + private redirectOverlayFocusOut( + { direction }: OverlayPortMessage, + { sender }: chrome.runtime.Port, + ) { + if (!direction) { + return; + } + + // 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 + BrowserApi.tabSendMessageData(sender.tab, "redirectOverlayFocusOut", { direction }); + } + + /** + * Triggers adding a new vault item from the overlay. Gathers data + * input by the user before calling to open the add/edit window. + * + * @param sender - The sender of the port message + */ + private getNewVaultItemDetails({ sender }: chrome.runtime.Port) { + void BrowserApi.tabSendMessage(sender.tab, { command: "addNewVaultItemFromOverlay" }); + } + + /** + * Handles adding a new vault item from the overlay. Gathers data login + * data captured in the extension message. + * + * @param login - The login data captured from the extension message + * @param sender - The sender of the extension message + */ + private async addNewVaultItem( + { login }: OverlayAddNewItemMessage, + sender: chrome.runtime.MessageSender, + ) { + if (!login) { + return; + } + + const uriView = new LoginUriView(); + uriView.uri = login.uri; + + const loginView = new LoginView(); + loginView.uris = [uriView]; + loginView.username = login.username || ""; + loginView.password = login.password || ""; + + const cipherView = new CipherView(); + cipherView.name = (Utils.getHostname(login.uri) || login.hostname).replace(/^www\./, ""); + cipherView.folderId = null; + cipherView.type = CipherType.Login; + cipherView.login = loginView; + + await this.cipherService.setAddEditCipherInfo({ + cipher: cipherView, + collectionIds: cipherView.collectionIds, + }); + + await this.openAddEditVaultItemPopout(sender.tab, { cipherId: cipherView.id }); + await BrowserApi.sendMessage("inlineAutofillMenuRefreshAddEditCipher"); + } + + /** + * Sets up the extension message listeners for the overlay. + */ + private setupExtensionMessageListeners() { + BrowserApi.messageListener("overlay.background", this.handleExtensionMessage); + BrowserApi.addListener(chrome.runtime.onConnect, this.handlePortOnConnect); + } + + /** + * Handles extension messages sent to the extension background. + * + * @param message - The message received from the extension + * @param sender - The sender of the message + * @param sendResponse - The response to send back to the sender + */ + private handleExtensionMessage = ( + message: OverlayBackgroundExtensionMessage, + sender: chrome.runtime.MessageSender, + sendResponse: (response?: any) => void, + ) => { + const handler: CallableFunction | undefined = this.extensionMessageHandlers[message?.command]; + if (!handler) { + return null; + } + + const messageResponse = handler({ message, sender }); + if (typeof messageResponse === "undefined") { + return null; + } + + // 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 + Promise.resolve(messageResponse).then((response) => sendResponse(response)); + return true; + }; + + /** + * Handles the connection of a port to the extension background. + * + * @param port - The port that connected to the extension background + */ + private handlePortOnConnect = async (port: chrome.runtime.Port) => { + const isOverlayListPort = port.name === AutofillOverlayPort.List; + const isOverlayButtonPort = port.name === AutofillOverlayPort.Button; + if (!isOverlayListPort && !isOverlayButtonPort) { + return; + } + + this.storeOverlayPort(port); + port.onMessage.addListener(this.handleOverlayElementPortMessage); + port.postMessage({ + command: `initAutofillOverlay${isOverlayListPort ? "List" : "Button"}`, + authStatus: await this.getAuthStatus(), + styleSheetUrl: chrome.runtime.getURL(`overlay/${isOverlayListPort ? "list" : "button"}.css`), + theme: await firstValueFrom(this.themeStateService.selectedTheme$), + translations: this.getTranslations(), + ciphers: isOverlayListPort ? await this.getOverlayCipherData() : null, + }); + this.updateOverlayPosition( + { + overlayElement: isOverlayListPort + ? AutofillOverlayElement.List + : AutofillOverlayElement.Button, + }, + port.sender, + ); + }; + + /** + * Stores the connected overlay port and sets up any existing ports to be disconnected. + * + * @param port - The port to store +| */ + private storeOverlayPort(port: chrome.runtime.Port) { + if (port.name === AutofillOverlayPort.List) { + this.storeExpiredOverlayPort(this.overlayListPort); + this.overlayListPort = port; + return; + } + + if (port.name === AutofillOverlayPort.Button) { + this.storeExpiredOverlayPort(this.overlayButtonPort); + this.overlayButtonPort = port; + } + } + + /** + * When registering a new connection, we want to ensure that the port is disconnected. + * This method places an existing port in the expiredPorts array to be disconnected + * at a later time. + * + * @param port - The port to store in the expiredPorts array + */ + private storeExpiredOverlayPort(port: chrome.runtime.Port | null) { + if (port) { + this.expiredPorts.push(port); + } + } + + /** + * Handles messages sent to the overlay list or button ports. + * + * @param message - The message received from the port + * @param port - The port that sent the message + */ + private handleOverlayElementPortMessage = ( + message: OverlayBackgroundExtensionMessage, + port: chrome.runtime.Port, + ) => { + const command = message?.command; + let handler: CallableFunction | undefined; + + if (port.name === AutofillOverlayPort.Button) { + handler = this.overlayButtonPortMessageHandlers[command]; + } + + if (port.name === AutofillOverlayPort.List) { + handler = this.overlayListPortMessageHandlers[command]; + } + + if (!handler) { + return; + } + + handler({ message, port }); + }; +} + +export default LegacyOverlayBackground; diff --git a/apps/browser/src/autofill/deprecated/content/abstractions/autofill-init.deprecated.ts b/apps/browser/src/autofill/deprecated/content/abstractions/autofill-init.deprecated.ts new file mode 100644 index 00000000000..ed422822b36 --- /dev/null +++ b/apps/browser/src/autofill/deprecated/content/abstractions/autofill-init.deprecated.ts @@ -0,0 +1,41 @@ +import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; + +import AutofillScript from "../../../models/autofill-script"; + +type AutofillExtensionMessage = { + command: string; + tab?: chrome.tabs.Tab; + sender?: string; + fillScript?: AutofillScript; + url?: string; + pageDetailsUrl?: string; + ciphers?: any; + data?: { + authStatus?: AuthenticationStatus; + isFocusingFieldElement?: boolean; + isOverlayCiphersPopulated?: boolean; + direction?: "previous" | "next"; + isOpeningFullOverlay?: boolean; + forceCloseOverlay?: boolean; + autofillOverlayVisibility?: number; + }; +}; + +type AutofillExtensionMessageParam = { message: AutofillExtensionMessage }; + +type AutofillExtensionMessageHandlers = { + [key: string]: CallableFunction; + collectPageDetails: ({ message }: AutofillExtensionMessageParam) => void; + collectPageDetailsImmediately: ({ message }: AutofillExtensionMessageParam) => void; + fillForm: ({ message }: AutofillExtensionMessageParam) => void; + openAutofillOverlay: ({ message }: AutofillExtensionMessageParam) => void; + closeAutofillOverlay: ({ message }: AutofillExtensionMessageParam) => void; + addNewVaultItemFromOverlay: () => void; + redirectOverlayFocusOut: ({ message }: AutofillExtensionMessageParam) => void; + updateIsOverlayCiphersPopulated: ({ message }: AutofillExtensionMessageParam) => void; + bgUnlockPopoutOpened: () => void; + bgVaultItemRepromptPopoutOpened: () => void; + updateAutofillOverlayVisibility: ({ message }: AutofillExtensionMessageParam) => void; +}; + +export { AutofillExtensionMessage, AutofillExtensionMessageHandlers }; diff --git a/apps/browser/src/autofill/deprecated/content/autofill-init.deprecated.spec.ts b/apps/browser/src/autofill/deprecated/content/autofill-init.deprecated.spec.ts new file mode 100644 index 00000000000..96d5e85ca34 --- /dev/null +++ b/apps/browser/src/autofill/deprecated/content/autofill-init.deprecated.spec.ts @@ -0,0 +1,604 @@ +import { mock } from "jest-mock-extended"; + +import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; +import { AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants"; + +import { RedirectFocusDirection } from "../../enums/autofill-overlay.enum"; +import AutofillPageDetails from "../../models/autofill-page-details"; +import AutofillScript from "../../models/autofill-script"; +import { + flushPromises, + mockQuerySelectorAllDefinedCall, + sendMockExtensionMessage, +} from "../../spec/testing-utils"; +import AutofillOverlayContentServiceDeprecated from "../services/autofill-overlay-content.service.deprecated"; + +import { AutofillExtensionMessage } from "./abstractions/autofill-init.deprecated"; +import AutofillInitDeprecated from "./autofill-init.deprecated"; + +describe("AutofillInit", () => { + let autofillInit: AutofillInitDeprecated; + const autofillOverlayContentService = mock(); + const originalDocumentReadyState = document.readyState; + const mockQuerySelectorAll = mockQuerySelectorAllDefinedCall(); + + beforeEach(() => { + chrome.runtime.connect = jest.fn().mockReturnValue({ + onDisconnect: { + addListener: jest.fn(), + }, + }); + autofillInit = new AutofillInitDeprecated(autofillOverlayContentService); + window.IntersectionObserver = jest.fn(() => mock()); + }); + + afterEach(() => { + jest.resetModules(); + jest.clearAllMocks(); + Object.defineProperty(document, "readyState", { + value: originalDocumentReadyState, + writable: true, + }); + }); + + afterAll(() => { + mockQuerySelectorAll.mockRestore(); + }); + + describe("init", () => { + it("sets up the extension message listeners", () => { + jest.spyOn(autofillInit as any, "setupExtensionMessageListeners"); + + autofillInit.init(); + + expect(autofillInit["setupExtensionMessageListeners"]).toHaveBeenCalled(); + }); + + it("triggers a collection of page details if the document is in a `complete` ready state", () => { + jest.useFakeTimers(); + Object.defineProperty(document, "readyState", { value: "complete", writable: true }); + + autofillInit.init(); + jest.advanceTimersByTime(250); + + expect(chrome.runtime.sendMessage).toHaveBeenCalledWith( + { + command: "bgCollectPageDetails", + sender: "autofillInit", + }, + expect.any(Function), + ); + }); + + it("registers a window load listener to collect the page details if the document is not in a `complete` ready state", () => { + jest.spyOn(window, "addEventListener"); + Object.defineProperty(document, "readyState", { value: "loading", writable: true }); + + autofillInit.init(); + + expect(window.addEventListener).toHaveBeenCalledWith("load", expect.any(Function)); + }); + }); + + describe("setupExtensionMessageListeners", () => { + it("sets up a chrome runtime on message listener", () => { + jest.spyOn(chrome.runtime.onMessage, "addListener"); + + autofillInit["setupExtensionMessageListeners"](); + + expect(chrome.runtime.onMessage.addListener).toHaveBeenCalledWith( + autofillInit["handleExtensionMessage"], + ); + }); + }); + + describe("handleExtensionMessage", () => { + let message: AutofillExtensionMessage; + let sender: chrome.runtime.MessageSender; + const sendResponse = jest.fn(); + + beforeEach(() => { + message = { + command: "collectPageDetails", + tab: mock(), + sender: "sender", + }; + sender = mock(); + }); + + it("returns a undefined value if a extension message handler is not found with the given message command", () => { + message.command = "unknownCommand"; + + const response = autofillInit["handleExtensionMessage"](message, sender, sendResponse); + + expect(response).toBe(null); + }); + + it("returns a undefined value if the message handler does not return a response", async () => { + const response1 = await autofillInit["handleExtensionMessage"](message, sender, sendResponse); + await flushPromises(); + + expect(response1).not.toBe(false); + + message.command = "removeAutofillOverlay"; + message.fillScript = mock(); + + const response2 = autofillInit["handleExtensionMessage"](message, sender, sendResponse); + await flushPromises(); + + expect(response2).toBe(null); + }); + + it("returns a true value and calls sendResponse if the message handler returns a response", async () => { + message.command = "collectPageDetailsImmediately"; + const pageDetails: AutofillPageDetails = { + title: "title", + url: "http://example.com", + documentUrl: "documentUrl", + forms: {}, + fields: [], + collectedTimestamp: 0, + }; + jest + .spyOn(autofillInit["collectAutofillContentService"], "getPageDetails") + .mockResolvedValue(pageDetails); + + const response = await autofillInit["handleExtensionMessage"](message, sender, sendResponse); + await flushPromises(); + + expect(response).toBe(true); + expect(sendResponse).toHaveBeenCalledWith(pageDetails); + }); + + describe("extension message handlers", () => { + beforeEach(() => { + autofillInit.init(); + }); + + describe("collectPageDetails", () => { + it("sends the collected page details for autofill using a background script message", async () => { + const pageDetails: AutofillPageDetails = { + title: "title", + url: "http://example.com", + documentUrl: "documentUrl", + forms: {}, + fields: [], + collectedTimestamp: 0, + }; + const message = { + command: "collectPageDetails", + sender: "sender", + tab: mock(), + }; + jest + .spyOn(autofillInit["collectAutofillContentService"], "getPageDetails") + .mockResolvedValue(pageDetails); + + sendMockExtensionMessage(message, sender, sendResponse); + await flushPromises(); + + expect(chrome.runtime.sendMessage).toHaveBeenCalledWith({ + command: "collectPageDetailsResponse", + tab: message.tab, + details: pageDetails, + sender: message.sender, + }); + }); + }); + + describe("collectPageDetailsImmediately", () => { + it("returns collected page details for autofill if set to send the details in the response", async () => { + const pageDetails: AutofillPageDetails = { + title: "title", + url: "http://example.com", + documentUrl: "documentUrl", + forms: {}, + fields: [], + collectedTimestamp: 0, + }; + jest + .spyOn(autofillInit["collectAutofillContentService"], "getPageDetails") + .mockResolvedValue(pageDetails); + + sendMockExtensionMessage( + { command: "collectPageDetailsImmediately" }, + sender, + sendResponse, + ); + await flushPromises(); + + expect(autofillInit["collectAutofillContentService"].getPageDetails).toHaveBeenCalled(); + expect(sendResponse).toBeCalledWith(pageDetails); + expect(chrome.runtime.sendMessage).not.toHaveBeenCalledWith({ + command: "collectPageDetailsResponse", + tab: message.tab, + details: pageDetails, + sender: message.sender, + }); + }); + }); + + describe("fillForm", () => { + let fillScript: AutofillScript; + beforeEach(() => { + fillScript = mock(); + jest.spyOn(autofillInit["insertAutofillContentService"], "fillForm").mockImplementation(); + }); + + it("skips calling the InsertAutofillContentService and does not fill the form if the url to fill is not equal to the current tab url", async () => { + const fillScript = mock(); + const message = { + command: "fillForm", + fillScript, + pageDetailsUrl: "https://a-different-url.com", + }; + + sendMockExtensionMessage(message); + await flushPromises(); + + expect(autofillInit["insertAutofillContentService"].fillForm).not.toHaveBeenCalledWith( + fillScript, + ); + }); + + it("calls the InsertAutofillContentService to fill the form", async () => { + sendMockExtensionMessage({ + command: "fillForm", + fillScript, + pageDetailsUrl: window.location.href, + }); + await flushPromises(); + + expect(autofillInit["insertAutofillContentService"].fillForm).toHaveBeenCalledWith( + fillScript, + ); + }); + + it("removes the overlay when filling the form", async () => { + const blurAndRemoveOverlaySpy = jest.spyOn(autofillInit as any, "blurAndRemoveOverlay"); + sendMockExtensionMessage({ + command: "fillForm", + fillScript, + pageDetailsUrl: window.location.href, + }); + await flushPromises(); + + expect(blurAndRemoveOverlaySpy).toHaveBeenCalled(); + }); + + it("updates the isCurrentlyFilling property of the overlay to true after filling", async () => { + jest.useFakeTimers(); + jest.spyOn(autofillInit as any, "updateOverlayIsCurrentlyFilling"); + jest + .spyOn(autofillInit["autofillOverlayContentService"], "focusMostRecentOverlayField") + .mockImplementation(); + + sendMockExtensionMessage({ + command: "fillForm", + fillScript, + pageDetailsUrl: window.location.href, + }); + await flushPromises(); + jest.advanceTimersByTime(300); + + expect(autofillInit["updateOverlayIsCurrentlyFilling"]).toHaveBeenNthCalledWith(1, true); + expect(autofillInit["insertAutofillContentService"].fillForm).toHaveBeenCalledWith( + fillScript, + ); + expect(autofillInit["updateOverlayIsCurrentlyFilling"]).toHaveBeenNthCalledWith(2, false); + }); + + it("skips attempting to focus the most recent field if the autofillOverlayContentService is not present", async () => { + jest.useFakeTimers(); + const newAutofillInit = new AutofillInitDeprecated(undefined); + newAutofillInit.init(); + jest.spyOn(newAutofillInit as any, "updateOverlayIsCurrentlyFilling"); + jest + .spyOn(newAutofillInit["insertAutofillContentService"], "fillForm") + .mockImplementation(); + + sendMockExtensionMessage({ + command: "fillForm", + fillScript, + pageDetailsUrl: window.location.href, + }); + await flushPromises(); + jest.advanceTimersByTime(300); + + expect(newAutofillInit["updateOverlayIsCurrentlyFilling"]).toHaveBeenNthCalledWith( + 1, + true, + ); + expect(newAutofillInit["insertAutofillContentService"].fillForm).toHaveBeenCalledWith( + fillScript, + ); + expect(newAutofillInit["updateOverlayIsCurrentlyFilling"]).not.toHaveBeenNthCalledWith( + 2, + false, + ); + }); + }); + + describe("openAutofillOverlay", () => { + const message = { + command: "openAutofillOverlay", + data: { + isFocusingFieldElement: true, + isOpeningFullOverlay: true, + authStatus: AuthenticationStatus.Unlocked, + }, + }; + + it("skips attempting to open the autofill overlay if the autofillOverlayContentService is not present", () => { + const newAutofillInit = new AutofillInitDeprecated(undefined); + newAutofillInit.init(); + + sendMockExtensionMessage(message); + + expect(newAutofillInit["autofillOverlayContentService"]).toBe(undefined); + }); + + it("opens the autofill overlay", () => { + sendMockExtensionMessage(message); + + expect( + autofillInit["autofillOverlayContentService"].openAutofillOverlay, + ).toHaveBeenCalledWith({ + isFocusingFieldElement: message.data.isFocusingFieldElement, + isOpeningFullOverlay: message.data.isOpeningFullOverlay, + authStatus: message.data.authStatus, + }); + }); + }); + + describe("closeAutofillOverlay", () => { + beforeEach(() => { + autofillInit["autofillOverlayContentService"].isFieldCurrentlyFocused = false; + autofillInit["autofillOverlayContentService"].isCurrentlyFilling = false; + }); + + it("skips attempting to remove the overlay if the autofillOverlayContentService is not present", () => { + const newAutofillInit = new AutofillInitDeprecated(undefined); + newAutofillInit.init(); + jest.spyOn(newAutofillInit as any, "removeAutofillOverlay"); + + sendMockExtensionMessage({ + command: "closeAutofillOverlay", + data: { forceCloseOverlay: false }, + }); + + expect(newAutofillInit["autofillOverlayContentService"]).toBe(undefined); + }); + + it("removes the autofill overlay if the message flags a forced closure", () => { + sendMockExtensionMessage({ + command: "closeAutofillOverlay", + data: { forceCloseOverlay: true }, + }); + + expect( + autofillInit["autofillOverlayContentService"].removeAutofillOverlay, + ).toHaveBeenCalled(); + }); + + it("ignores the message if a field is currently focused", () => { + autofillInit["autofillOverlayContentService"].isFieldCurrentlyFocused = true; + + sendMockExtensionMessage({ command: "closeAutofillOverlay" }); + + expect( + autofillInit["autofillOverlayContentService"].removeAutofillOverlayList, + ).not.toHaveBeenCalled(); + expect( + autofillInit["autofillOverlayContentService"].removeAutofillOverlay, + ).not.toHaveBeenCalled(); + }); + + it("removes the autofill overlay list if the overlay is currently filling", () => { + autofillInit["autofillOverlayContentService"].isCurrentlyFilling = true; + + sendMockExtensionMessage({ command: "closeAutofillOverlay" }); + + expect( + autofillInit["autofillOverlayContentService"].removeAutofillOverlayList, + ).toHaveBeenCalled(); + expect( + autofillInit["autofillOverlayContentService"].removeAutofillOverlay, + ).not.toHaveBeenCalled(); + }); + + it("removes the entire overlay if the overlay is not currently filling", () => { + sendMockExtensionMessage({ command: "closeAutofillOverlay" }); + + expect( + autofillInit["autofillOverlayContentService"].removeAutofillOverlayList, + ).not.toHaveBeenCalled(); + expect( + autofillInit["autofillOverlayContentService"].removeAutofillOverlay, + ).toHaveBeenCalled(); + }); + }); + + describe("addNewVaultItemFromOverlay", () => { + it("will not add a new vault item if the autofillOverlayContentService is not present", () => { + const newAutofillInit = new AutofillInitDeprecated(undefined); + newAutofillInit.init(); + + sendMockExtensionMessage({ command: "addNewVaultItemFromOverlay" }); + + expect(newAutofillInit["autofillOverlayContentService"]).toBe(undefined); + }); + + it("will add a new vault item", () => { + sendMockExtensionMessage({ command: "addNewVaultItemFromOverlay" }); + + expect(autofillInit["autofillOverlayContentService"].addNewVaultItem).toHaveBeenCalled(); + }); + }); + + describe("redirectOverlayFocusOut", () => { + const message = { + command: "redirectOverlayFocusOut", + data: { + direction: RedirectFocusDirection.Next, + }, + }; + + it("ignores the message to redirect focus if the autofillOverlayContentService does not exist", () => { + const newAutofillInit = new AutofillInitDeprecated(undefined); + newAutofillInit.init(); + + sendMockExtensionMessage(message); + + expect(newAutofillInit["autofillOverlayContentService"]).toBe(undefined); + }); + + it("redirects the overlay focus", () => { + sendMockExtensionMessage(message); + + expect( + autofillInit["autofillOverlayContentService"].redirectOverlayFocusOut, + ).toHaveBeenCalledWith(message.data.direction); + }); + }); + + describe("updateIsOverlayCiphersPopulated", () => { + const message = { + command: "updateIsOverlayCiphersPopulated", + data: { + isOverlayCiphersPopulated: true, + }, + }; + + it("skips updating whether the ciphers are populated if the autofillOverlayContentService does note exist", () => { + const newAutofillInit = new AutofillInitDeprecated(undefined); + newAutofillInit.init(); + + sendMockExtensionMessage(message); + + expect(newAutofillInit["autofillOverlayContentService"]).toBe(undefined); + }); + + it("updates whether the overlay ciphers are populated", () => { + sendMockExtensionMessage(message); + + expect(autofillInit["autofillOverlayContentService"].isOverlayCiphersPopulated).toEqual( + message.data.isOverlayCiphersPopulated, + ); + }); + }); + + describe("bgUnlockPopoutOpened", () => { + it("skips attempting to blur and remove the overlay if the autofillOverlayContentService is not present", () => { + const newAutofillInit = new AutofillInitDeprecated(undefined); + newAutofillInit.init(); + jest.spyOn(newAutofillInit as any, "removeAutofillOverlay"); + + sendMockExtensionMessage({ command: "bgUnlockPopoutOpened" }); + + expect(newAutofillInit["autofillOverlayContentService"]).toBe(undefined); + expect(newAutofillInit["removeAutofillOverlay"]).not.toHaveBeenCalled(); + }); + + it("blurs the most recently focused feel and remove the autofill overlay", () => { + jest.spyOn(autofillInit["autofillOverlayContentService"], "blurMostRecentOverlayField"); + jest.spyOn(autofillInit as any, "removeAutofillOverlay"); + + sendMockExtensionMessage({ command: "bgUnlockPopoutOpened" }); + + expect( + autofillInit["autofillOverlayContentService"].blurMostRecentOverlayField, + ).toHaveBeenCalled(); + expect(autofillInit["removeAutofillOverlay"]).toHaveBeenCalled(); + }); + }); + + describe("bgVaultItemRepromptPopoutOpened", () => { + it("skips attempting to blur and remove the overlay if the autofillOverlayContentService is not present", () => { + const newAutofillInit = new AutofillInitDeprecated(undefined); + newAutofillInit.init(); + jest.spyOn(newAutofillInit as any, "removeAutofillOverlay"); + + sendMockExtensionMessage({ command: "bgVaultItemRepromptPopoutOpened" }); + + expect(newAutofillInit["autofillOverlayContentService"]).toBe(undefined); + expect(newAutofillInit["removeAutofillOverlay"]).not.toHaveBeenCalled(); + }); + + it("blurs the most recently focused feel and remove the autofill overlay", () => { + jest.spyOn(autofillInit["autofillOverlayContentService"], "blurMostRecentOverlayField"); + jest.spyOn(autofillInit as any, "removeAutofillOverlay"); + + sendMockExtensionMessage({ command: "bgVaultItemRepromptPopoutOpened" }); + + expect( + autofillInit["autofillOverlayContentService"].blurMostRecentOverlayField, + ).toHaveBeenCalled(); + expect(autofillInit["removeAutofillOverlay"]).toHaveBeenCalled(); + }); + }); + + describe("updateAutofillOverlayVisibility", () => { + beforeEach(() => { + autofillInit["autofillOverlayContentService"].autofillOverlayVisibility = + AutofillOverlayVisibility.OnButtonClick; + }); + + it("skips attempting to update the overlay visibility if the autofillOverlayVisibility data value is not present", () => { + sendMockExtensionMessage({ + command: "updateAutofillOverlayVisibility", + data: {}, + }); + + expect(autofillInit["autofillOverlayContentService"].autofillOverlayVisibility).toEqual( + AutofillOverlayVisibility.OnButtonClick, + ); + }); + + it("updates the overlay visibility value", () => { + const message = { + command: "updateAutofillOverlayVisibility", + data: { + autofillOverlayVisibility: AutofillOverlayVisibility.Off, + }, + }; + + sendMockExtensionMessage(message); + + expect(autofillInit["autofillOverlayContentService"].autofillOverlayVisibility).toEqual( + message.data.autofillOverlayVisibility, + ); + }); + }); + }); + }); + + describe("destroy", () => { + it("clears the timeout used to collect page details on load", () => { + jest.spyOn(window, "clearTimeout"); + + autofillInit.init(); + autofillInit.destroy(); + + expect(window.clearTimeout).toHaveBeenCalledWith( + autofillInit["collectPageDetailsOnLoadTimeout"], + ); + }); + + it("removes the extension message listeners", () => { + autofillInit.destroy(); + + expect(chrome.runtime.onMessage.removeListener).toHaveBeenCalledWith( + autofillInit["handleExtensionMessage"], + ); + }); + + it("destroys the collectAutofillContentService", () => { + jest.spyOn(autofillInit["collectAutofillContentService"], "destroy"); + + autofillInit.destroy(); + + expect(autofillInit["collectAutofillContentService"].destroy).toHaveBeenCalled(); + }); + }); +}); diff --git a/apps/browser/src/autofill/deprecated/content/autofill-init.deprecated.ts b/apps/browser/src/autofill/deprecated/content/autofill-init.deprecated.ts new file mode 100644 index 00000000000..3e36fa43bbd --- /dev/null +++ b/apps/browser/src/autofill/deprecated/content/autofill-init.deprecated.ts @@ -0,0 +1,310 @@ +import { AutofillInit } from "../../content/abstractions/autofill-init"; +import AutofillPageDetails from "../../models/autofill-page-details"; +import CollectAutofillContentService from "../../services/collect-autofill-content.service"; +import DomElementVisibilityService from "../../services/dom-element-visibility.service"; +import InsertAutofillContentService from "../../services/insert-autofill-content.service"; +import { sendExtensionMessage } from "../../utils"; +import { LegacyAutofillOverlayContentService } from "../services/abstractions/autofill-overlay-content.service"; + +import { + AutofillExtensionMessage, + AutofillExtensionMessageHandlers, +} from "./abstractions/autofill-init.deprecated"; + +class LegacyAutofillInit implements AutofillInit { + private readonly autofillOverlayContentService: LegacyAutofillOverlayContentService | undefined; + private readonly domElementVisibilityService: DomElementVisibilityService; + private readonly collectAutofillContentService: CollectAutofillContentService; + private readonly insertAutofillContentService: InsertAutofillContentService; + private collectPageDetailsOnLoadTimeout: number | NodeJS.Timeout | undefined; + private readonly extensionMessageHandlers: AutofillExtensionMessageHandlers = { + collectPageDetails: ({ message }) => this.collectPageDetails(message), + collectPageDetailsImmediately: ({ message }) => this.collectPageDetails(message, true), + fillForm: ({ message }) => this.fillForm(message), + openAutofillOverlay: ({ message }) => this.openAutofillOverlay(message), + closeAutofillOverlay: ({ message }) => this.removeAutofillOverlay(message), + addNewVaultItemFromOverlay: () => this.addNewVaultItemFromOverlay(), + redirectOverlayFocusOut: ({ message }) => this.redirectOverlayFocusOut(message), + updateIsOverlayCiphersPopulated: ({ message }) => this.updateIsOverlayCiphersPopulated(message), + bgUnlockPopoutOpened: () => this.blurAndRemoveOverlay(), + bgVaultItemRepromptPopoutOpened: () => this.blurAndRemoveOverlay(), + updateAutofillOverlayVisibility: ({ message }) => this.updateAutofillOverlayVisibility(message), + }; + + /** + * AutofillInit constructor. Initializes the DomElementVisibilityService, + * CollectAutofillContentService and InsertAutofillContentService classes. + * + * @param autofillOverlayContentService - The autofill overlay content service, potentially undefined. + */ + constructor(autofillOverlayContentService?: LegacyAutofillOverlayContentService) { + this.autofillOverlayContentService = autofillOverlayContentService; + this.domElementVisibilityService = new DomElementVisibilityService(); + this.collectAutofillContentService = new CollectAutofillContentService( + this.domElementVisibilityService, + this.autofillOverlayContentService, + ); + this.insertAutofillContentService = new InsertAutofillContentService( + this.domElementVisibilityService, + this.collectAutofillContentService, + ); + } + + /** + * Initializes the autofill content script, setting up + * the extension message listeners. This method should + * be called once when the content script is loaded. + */ + init() { + this.setupExtensionMessageListeners(); + this.autofillOverlayContentService?.init(); + this.collectPageDetailsOnLoad(); + } + + /** + * Triggers a collection of the page details from the + * background script, ensuring that autofill is ready + * to act on the page. + */ + private collectPageDetailsOnLoad() { + const sendCollectDetailsMessage = () => { + this.clearCollectPageDetailsOnLoadTimeout(); + this.collectPageDetailsOnLoadTimeout = setTimeout( + () => sendExtensionMessage("bgCollectPageDetails", { sender: "autofillInit" }), + 250, + ); + }; + + if (globalThis.document.readyState === "complete") { + sendCollectDetailsMessage(); + } + + globalThis.addEventListener("load", sendCollectDetailsMessage); + } + + /** + * Collects the page details and sends them to the + * extension background script. If the `sendDetailsInResponse` + * parameter is set to true, the page details will be + * returned to facilitate sending the details in the + * response to the extension message. + * + * @param message - The extension message. + * @param sendDetailsInResponse - Determines whether to send the details in the response. + */ + private async collectPageDetails( + message: AutofillExtensionMessage, + sendDetailsInResponse = false, + ): Promise { + const pageDetails: AutofillPageDetails = + await this.collectAutofillContentService.getPageDetails(); + if (sendDetailsInResponse) { + return pageDetails; + } + + void chrome.runtime.sendMessage({ + command: "collectPageDetailsResponse", + tab: message.tab, + details: pageDetails, + sender: message.sender, + }); + } + + /** + * Fills the form with the given fill script. + * + * @param {AutofillExtensionMessage} message + */ + private async fillForm({ fillScript, pageDetailsUrl }: AutofillExtensionMessage) { + if ((document.defaultView || window).location.href !== pageDetailsUrl) { + return; + } + + this.blurAndRemoveOverlay(); + this.updateOverlayIsCurrentlyFilling(true); + await this.insertAutofillContentService.fillForm(fillScript); + + if (!this.autofillOverlayContentService) { + return; + } + + setTimeout(() => this.updateOverlayIsCurrentlyFilling(false), 250); + } + + /** + * Handles updating the overlay is currently filling value. + * + * @param isCurrentlyFilling - Indicates if the overlay is currently filling + */ + private updateOverlayIsCurrentlyFilling(isCurrentlyFilling: boolean) { + if (!this.autofillOverlayContentService) { + return; + } + + this.autofillOverlayContentService.isCurrentlyFilling = isCurrentlyFilling; + } + + /** + * Opens the autofill overlay. + * + * @param data - The extension message data. + */ + private openAutofillOverlay({ data }: AutofillExtensionMessage) { + if (!this.autofillOverlayContentService) { + return; + } + + this.autofillOverlayContentService.openAutofillOverlay(data); + } + + /** + * Blurs the most recent overlay field and removes the overlay. Used + * in cases where the background unlock or vault item reprompt popout + * is opened. + */ + private blurAndRemoveOverlay() { + if (!this.autofillOverlayContentService) { + return; + } + + this.autofillOverlayContentService.blurMostRecentOverlayField(); + this.removeAutofillOverlay(); + } + + /** + * Removes the autofill overlay if the field is not currently focused. + * If the autofill is currently filling, only the overlay list will be + * removed. + */ + private removeAutofillOverlay(message?: AutofillExtensionMessage) { + if (message?.data?.forceCloseOverlay) { + this.autofillOverlayContentService?.removeAutofillOverlay(); + return; + } + + if ( + !this.autofillOverlayContentService || + this.autofillOverlayContentService.isFieldCurrentlyFocused + ) { + return; + } + + if (this.autofillOverlayContentService.isCurrentlyFilling) { + this.autofillOverlayContentService.removeAutofillOverlayList(); + return; + } + + this.autofillOverlayContentService.removeAutofillOverlay(); + } + + /** + * Adds a new vault item from the overlay. + */ + private addNewVaultItemFromOverlay() { + if (!this.autofillOverlayContentService) { + return; + } + + this.autofillOverlayContentService.addNewVaultItem(); + } + + /** + * Redirects the overlay focus out of an overlay iframe. + * + * @param data - Contains the direction to redirect the focus. + */ + private redirectOverlayFocusOut({ data }: AutofillExtensionMessage) { + if (!this.autofillOverlayContentService) { + return; + } + + this.autofillOverlayContentService.redirectOverlayFocusOut(data?.direction); + } + + /** + * Updates whether the current tab has ciphers that can populate the overlay list + * + * @param data - Contains the isOverlayCiphersPopulated value + * + */ + private updateIsOverlayCiphersPopulated({ data }: AutofillExtensionMessage) { + if (!this.autofillOverlayContentService) { + return; + } + + this.autofillOverlayContentService.isOverlayCiphersPopulated = Boolean( + data?.isOverlayCiphersPopulated, + ); + } + + /** + * Updates the autofill overlay visibility. + * + * @param data - Contains the autoFillOverlayVisibility value + */ + private updateAutofillOverlayVisibility({ data }: AutofillExtensionMessage) { + if (!this.autofillOverlayContentService || isNaN(data?.autofillOverlayVisibility)) { + return; + } + + this.autofillOverlayContentService.autofillOverlayVisibility = data?.autofillOverlayVisibility; + } + + /** + * Clears the send collect details message timeout. + */ + private clearCollectPageDetailsOnLoadTimeout() { + if (this.collectPageDetailsOnLoadTimeout) { + clearTimeout(this.collectPageDetailsOnLoadTimeout); + } + } + + /** + * Sets up the extension message listeners for the content script. + */ + private setupExtensionMessageListeners() { + chrome.runtime.onMessage.addListener(this.handleExtensionMessage); + } + + /** + * Handles the extension messages sent to the content script. + * + * @param message - The extension message. + * @param sender - The message sender. + * @param sendResponse - The send response callback. + */ + private handleExtensionMessage = ( + message: AutofillExtensionMessage, + sender: chrome.runtime.MessageSender, + sendResponse: (response?: any) => void, + ): boolean => { + const command: string = message.command; + const handler: CallableFunction | undefined = this.extensionMessageHandlers[command]; + if (!handler) { + return null; + } + + const messageResponse = handler({ message, sender }); + if (typeof messageResponse === "undefined") { + return null; + } + + // 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 + Promise.resolve(messageResponse).then((response) => sendResponse(response)); + return true; + }; + + /** + * Handles destroying the autofill init content script. Removes all + * listeners, timeouts, and object instances to prevent memory leaks. + */ + destroy() { + this.clearCollectPageDetailsOnLoadTimeout(); + chrome.runtime.onMessage.removeListener(this.handleExtensionMessage); + this.collectAutofillContentService.destroy(); + this.autofillOverlayContentService?.destroy(); + } +} + +export default LegacyAutofillInit; diff --git a/apps/browser/src/autofill/deprecated/content/bootstrap-legacy-autofill-overlay.ts b/apps/browser/src/autofill/deprecated/content/bootstrap-legacy-autofill-overlay.ts new file mode 100644 index 00000000000..66d672172ae --- /dev/null +++ b/apps/browser/src/autofill/deprecated/content/bootstrap-legacy-autofill-overlay.ts @@ -0,0 +1,14 @@ +import { setupAutofillInitDisconnectAction } from "../../utils"; +import LegacyAutofillOverlayContentService from "../services/autofill-overlay-content.service.deprecated"; + +import LegacyAutofillInit from "./autofill-init.deprecated"; + +(function (windowContext) { + if (!windowContext.bitwardenAutofillInit) { + const autofillOverlayContentService = new LegacyAutofillOverlayContentService(); + windowContext.bitwardenAutofillInit = new LegacyAutofillInit(autofillOverlayContentService); + setupAutofillInitDisconnectAction(windowContext); + + windowContext.bitwardenAutofillInit.init(); + } +})(window); diff --git a/apps/browser/src/autofill/overlay/abstractions/autofill-overlay-button.ts b/apps/browser/src/autofill/deprecated/overlay/abstractions/autofill-overlay-button.deprecated.ts similarity index 100% rename from apps/browser/src/autofill/overlay/abstractions/autofill-overlay-button.ts rename to apps/browser/src/autofill/deprecated/overlay/abstractions/autofill-overlay-button.deprecated.ts diff --git a/apps/browser/src/autofill/overlay/abstractions/autofill-overlay-iframe.service.ts b/apps/browser/src/autofill/deprecated/overlay/abstractions/autofill-overlay-iframe.service.deprecated.ts similarity index 100% rename from apps/browser/src/autofill/overlay/abstractions/autofill-overlay-iframe.service.ts rename to apps/browser/src/autofill/deprecated/overlay/abstractions/autofill-overlay-iframe.service.deprecated.ts diff --git a/apps/browser/src/autofill/overlay/abstractions/autofill-overlay-list.ts b/apps/browser/src/autofill/deprecated/overlay/abstractions/autofill-overlay-list.deprecated.ts similarity index 96% rename from apps/browser/src/autofill/overlay/abstractions/autofill-overlay-list.ts rename to apps/browser/src/autofill/deprecated/overlay/abstractions/autofill-overlay-list.deprecated.ts index b656f238dce..83578b13043 100644 --- a/apps/browser/src/autofill/overlay/abstractions/autofill-overlay-list.ts +++ b/apps/browser/src/autofill/deprecated/overlay/abstractions/autofill-overlay-list.deprecated.ts @@ -1,6 +1,6 @@ import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; -import { OverlayCipherData } from "../../background/abstractions/overlay.background"; +import { OverlayCipherData } from "../../background/abstractions/overlay.background.deprecated"; type OverlayListMessage = { command: string }; diff --git a/apps/browser/src/autofill/overlay/abstractions/autofill-overlay-page-element.ts b/apps/browser/src/autofill/deprecated/overlay/abstractions/autofill-overlay-page-element.deprecated.ts similarity index 89% rename from apps/browser/src/autofill/overlay/abstractions/autofill-overlay-page-element.ts rename to apps/browser/src/autofill/deprecated/overlay/abstractions/autofill-overlay-page-element.deprecated.ts index eb3c2fa4a71..368ae4e7303 100644 --- a/apps/browser/src/autofill/overlay/abstractions/autofill-overlay-page-element.ts +++ b/apps/browser/src/autofill/deprecated/overlay/abstractions/autofill-overlay-page-element.deprecated.ts @@ -1,5 +1,5 @@ -import { OverlayButtonWindowMessageHandlers } from "./autofill-overlay-button"; -import { OverlayListWindowMessageHandlers } from "./autofill-overlay-list"; +import { OverlayButtonWindowMessageHandlers } from "./autofill-overlay-button.deprecated"; +import { OverlayListWindowMessageHandlers } from "./autofill-overlay-list.deprecated"; type WindowMessageHandlers = OverlayButtonWindowMessageHandlers | OverlayListWindowMessageHandlers; diff --git a/apps/browser/src/autofill/overlay/iframe-content/__snapshots__/autofill-overlay-iframe.service.spec.ts.snap b/apps/browser/src/autofill/deprecated/overlay/iframe-content/__snapshots__/autofill-overlay-iframe.service.deprecated.spec.ts.snap similarity index 95% rename from apps/browser/src/autofill/overlay/iframe-content/__snapshots__/autofill-overlay-iframe.service.spec.ts.snap rename to apps/browser/src/autofill/deprecated/overlay/iframe-content/__snapshots__/autofill-overlay-iframe.service.deprecated.spec.ts.snap index cb8e4a541bb..132bd968899 100644 --- a/apps/browser/src/autofill/overlay/iframe-content/__snapshots__/autofill-overlay-iframe.service.spec.ts.snap +++ b/apps/browser/src/autofill/deprecated/overlay/iframe-content/__snapshots__/autofill-overlay-iframe.service.deprecated.spec.ts.snap @@ -15,7 +15,7 @@ exports[`AutofillOverlayIframeService initOverlayIframe sets up the iframe's att `; + }); + + it("returns null if the sub frame URL cannot be parsed correctly", async () => { + delete globalThis.location; + globalThis.location = { href: "invalid-base" } as Location; + sendMockExtensionMessage( + { + command: "getSubFrameOffsets", + subFrameUrl: iframeSource, + }, + mock(), + sendResponseSpy, + ); + await flushPromises(); + + expect(sendResponseSpy).toHaveBeenCalledWith(null); + }); + + it("calculates the sub frame's offsets if a single frame with the referenced url exists", async () => { + sendMockExtensionMessage( + { + command: "getSubFrameOffsets", + subFrameUrl: iframeSource, + }, + mock(), + sendResponseSpy, + ); + await flushPromises(); + + expect(sendResponseSpy).toHaveBeenCalledWith({ + frameId: undefined, + left: 2, + top: 2, + url: iframeSource, + }); + }); + + it("returns null if a matching iframe is not found", async () => { + document.body.innerHTML = ""; + sendMockExtensionMessage( + { + command: "getSubFrameOffsets", + subFrameUrl: iframeSource, + }, + mock(), + sendResponseSpy, + ); + await flushPromises(); + + expect(sendResponseSpy).toHaveBeenCalledWith(null); + }); + + it("returns null if two or more iframes are found with the same src", async () => { + document.body.innerHTML = ` + + + `; + + sendMockExtensionMessage( + { + command: "getSubFrameOffsets", + subFrameUrl: iframeSource, + }, + mock(), + sendResponseSpy, + ); + await flushPromises(); + + expect(sendResponseSpy).toHaveBeenCalledWith(null); + }); + }); + + describe("getSubFrameOffsetsFromWindowMessage", () => { + it("sends a message to the parent to calculate the sub frame positioning", () => { + jest.spyOn(globalThis.parent, "postMessage").mockImplementation(); + const subFrameId = 10; + + sendMockExtensionMessage({ + command: "getSubFrameOffsetsFromWindowMessage", + subFrameId, + }); + + expect(globalThis.parent.postMessage).toHaveBeenCalledWith( + { + command: "calculateSubFramePositioning", + subFrameData: { + url: window.location.href, + frameId: subFrameId, + left: 0, + top: 0, + parentFrameIds: [0], + subFrameDepth: 0, + }, + }, + "*", + ); + }); + + describe("calculateSubFramePositioning", () => { + beforeEach(() => { + autofillOverlayContentService.init(); + jest.spyOn(globalThis.parent, "postMessage"); + document.body.innerHTML = ``; + }); + + it("destroys the inline menu listeners on the origin frame if the depth exceeds the threshold", async () => { + document.body.innerHTML = ``; + const iframe = document.querySelector("iframe") as HTMLIFrameElement; + const subFrameData = { + url: "https://example.com/", + frameId: 10, + left: 0, + top: 0, + parentFrameIds: [1, 2, 3], + subFrameDepth: MAX_SUB_FRAME_DEPTH, + }; + sendExtensionMessageSpy.mockResolvedValue(4); + + postWindowMessage( + { command: "calculateSubFramePositioning", subFrameData }, + "*", + iframe.contentWindow as any, + ); + await flushPromises(); + + expect(globalThis.parent.postMessage).not.toHaveBeenCalled(); + }); + + it("calculates the sub frame offset for the current frame and sends those values to the parent if not in the top frame", async () => { + Object.defineProperty(window, "top", { + value: null, + writable: true, + }); + document.body.innerHTML = ``; + const iframe = document.querySelector("iframe") as HTMLIFrameElement; + const subFrameData = { + url: "https://example.com/", + frameId: 10, + left: 0, + top: 0, + parentFrameIds: [1, 2, 3], + subFrameDepth: 0, + }; + + postWindowMessage( + { command: "calculateSubFramePositioning", subFrameData }, + "*", + iframe.contentWindow as any, + ); + await flushPromises(); + + expect(globalThis.parent.postMessage).toHaveBeenCalledWith( + { + command: "calculateSubFramePositioning", + subFrameData: { + frameId: 10, + left: expect.any(Number), + parentFrameIds: [1, 2, 3], + top: expect.any(Number), + url: "https://example.com/", + subFrameDepth: expect.any(Number), + }, + }, + "*", + ); + }); + + it("posts the calculated sub frame data to the background", async () => { + document.body.innerHTML = ``; + const iframe = document.querySelector("iframe") as HTMLIFrameElement; + const subFrameData = { + url: "https://example.com/", + frameId: 10, + left: 0, + top: 0, + parentFrameIds: [1, 2, 3], + subFrameDepth: expect.any(Number), + }; + + postWindowMessage( + { command: "calculateSubFramePositioning", subFrameData }, + "*", + iframe.contentWindow as any, + ); + await flushPromises(); + + expect(sendExtensionMessageSpy).toHaveBeenCalledWith("updateSubFrameData", { + subFrameData: { + frameId: 10, + left: expect.any(Number), + top: expect.any(Number), + url: "https://example.com/", + parentFrameIds: [1, 2, 3, 4], + subFrameDepth: expect.any(Number), + }, + }); + }); + }); + }); + + describe("checkMostRecentlyFocusedFieldHasValue message handler", () => { + it("returns true if the most recently focused field has a truthy value", async () => { + autofillOverlayContentService["mostRecentlyFocusedField"] = mock< + ElementWithOpId + >({ value: "test" }); + + sendMockExtensionMessage( + { + command: "checkMostRecentlyFocusedFieldHasValue", + }, + mock(), + sendResponseSpy, + ); + await flushPromises(); + + expect(sendResponseSpy).toHaveBeenCalledWith(true); + }); + }); + + describe("setupRebuildSubFrameOffsetsListeners message handler", () => { + let autofillFieldElement: ElementWithOpId; + + beforeEach(() => { + Object.defineProperty(window, "top", { + value: null, + writable: true, + }); + jest.spyOn(globalThis, "addEventListener"); + jest.spyOn(globalThis.document.body, "addEventListener"); + document.body.innerHTML = ` +
+ + +
+ `; + autofillFieldElement = document.getElementById( + "username-field", + ) as ElementWithOpId; + }); + + describe("skipping the setup of the sub frame listeners", () => { + it('skips setup when the window is the "top" frame', async () => { + Object.defineProperty(window, "top", { + value: window, + writable: true, + }); + + sendMockExtensionMessage({ command: "setupRebuildSubFrameOffsetsListeners" }); + await flushPromises(); + + expect(globalThis.addEventListener).not.toHaveBeenCalledWith( + EVENTS.FOCUS, + expect.any(Function), + ); + expect(globalThis.document.body.addEventListener).not.toHaveBeenCalledWith( + EVENTS.MOUSEENTER, + expect.any(Function), + ); + }); + + it("skips setup when no form fields exist on the current frame", async () => { + autofillOverlayContentService["formFieldElements"] = new Set(); + + sendMockExtensionMessage({ command: "setupRebuildSubFrameOffsetsListeners" }); + await flushPromises(); + + expect(globalThis.addEventListener).not.toHaveBeenCalledWith( + EVENTS.FOCUS, + expect.any(Function), + ); + expect(globalThis.document.body.addEventListener).not.toHaveBeenCalledWith( + EVENTS.MOUSEENTER, + expect.any(Function), + ); + }); + }); + + it("sets up the sub frame rebuild listeners when the sub frame contains fields", async () => { + autofillOverlayContentService["formFieldElements"].add(autofillFieldElement); + + sendMockExtensionMessage({ command: "setupRebuildSubFrameOffsetsListeners" }); + await flushPromises(); + + expect(globalThis.addEventListener).toHaveBeenCalledWith( + EVENTS.FOCUS, + expect.any(Function), + ); + expect(globalThis.document.body.addEventListener).toHaveBeenCalledWith( + EVENTS.MOUSEENTER, + expect.any(Function), + ); + }); + + describe("triggering the sub frame listener", () => { + beforeEach(async () => { + autofillOverlayContentService["formFieldElements"].add(autofillFieldElement); + await sendMockExtensionMessage({ command: "setupRebuildSubFrameOffsetsListeners" }); + }); + + it("triggers a rebuild of the sub frame listener when a focus event occurs", async () => { + globalThis.dispatchEvent(new Event(EVENTS.FOCUS)); + await flushPromises(); + + expect(sendExtensionMessageSpy).toHaveBeenCalledWith("triggerSubFrameFocusInRebuild"); + }); + }); + }); + + describe("destroyAutofillInlineMenuListeners message handler", () => { + it("destroys the inline menu listeners", () => { + jest.spyOn(autofillOverlayContentService, "destroy"); + + sendMockExtensionMessage({ command: "destroyAutofillInlineMenuListeners" }); + + expect(autofillOverlayContentService.destroy).toHaveBeenCalled(); + }); }); }); @@ -1670,36 +1679,18 @@ describe("AutofillOverlayContentService", () => { forms: { validFormId: mock() }, fields: [autofillFieldData, passwordFieldData], }); - void autofillOverlayContentService.setupAutofillOverlayListenerOnField( + void autofillOverlayContentService.setupInlineMenu( autofillFieldElement, autofillFieldData, pageDetailsMock, ); autofillOverlayContentService["mostRecentlyFocusedField"] = autofillFieldElement; - }); - - it("disconnects all mutation observers", () => { - autofillOverlayContentService["setupMutationObserver"](); - jest.spyOn(autofillOverlayContentService["bodyElementMutationObserver"], "disconnect"); - - autofillOverlayContentService.destroy(); - - expect( - autofillOverlayContentService["bodyElementMutationObserver"].disconnect, - ).toHaveBeenCalled(); - }); - - it("clears the user interaction event timeout", () => { - jest.spyOn(autofillOverlayContentService as any, "clearUserInteractionEventTimeout"); - - autofillOverlayContentService.destroy(); - - expect(autofillOverlayContentService["clearUserInteractionEventTimeout"]).toHaveBeenCalled(); + jest.spyOn(globalThis, "clearTimeout"); + jest.spyOn(globalThis.document, "removeEventListener"); + jest.spyOn(globalThis, "removeEventListener"); }); it("de-registers all global event listeners", () => { - jest.spyOn(globalThis.document, "removeEventListener"); - jest.spyOn(globalThis, "removeEventListener"); jest.spyOn(autofillOverlayContentService as any, "removeOverlayRepositionEventListeners"); autofillOverlayContentService.destroy(); @@ -1739,5 +1730,22 @@ describe("AutofillOverlayContentService", () => { autofillFieldElement, ); }); + + it("clears all existing timeouts", () => { + autofillOverlayContentService["focusInlineMenuListTimeout"] = setTimeout(jest.fn(), 100); + autofillOverlayContentService["closeInlineMenuOnRedirectTimeout"] = setTimeout( + jest.fn(), + 100, + ); + + autofillOverlayContentService.destroy(); + + expect(clearTimeout).toHaveBeenCalledWith( + autofillOverlayContentService["focusInlineMenuListTimeout"], + ); + expect(clearTimeout).toHaveBeenCalledWith( + autofillOverlayContentService["closeInlineMenuOnRedirectTimeout"], + ); + }); }); }); 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 d56a8a80cc6..8148ab98d8a 100644 --- a/apps/browser/src/autofill/services/autofill-overlay-content.service.ts +++ b/apps/browser/src/autofill/services/autofill-overlay-content.service.ts @@ -3,71 +3,79 @@ import "lit/polyfill-support.js"; import { FocusableElement, tabbable } from "tabbable"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; -import { EVENTS, AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants"; +import { + EVENTS, + AutofillOverlayVisibility, + AUTOFILL_OVERLAY_HANDLE_REPOSITION, +} from "@bitwarden/common/autofill/constants"; -import { FocusedFieldData } from "../background/abstractions/overlay.background"; +import { + FocusedFieldData, + SubFrameOffsetData, +} from "../background/abstractions/overlay.background"; +import { AutofillExtensionMessage } from "../content/abstractions/autofill-init"; +import { + AutofillOverlayElement, + MAX_SUB_FRAME_DEPTH, + RedirectFocusDirection, +} from "../enums/autofill-overlay.enum"; import AutofillField from "../models/autofill-field"; import AutofillPageDetails from "../models/autofill-page-details"; -import AutofillOverlayButtonIframe from "../overlay/iframe-content/autofill-overlay-button-iframe"; -import AutofillOverlayListIframe from "../overlay/iframe-content/autofill-overlay-list-iframe"; import { ElementWithOpId, FillableFormFieldElement, FormFieldElement } from "../types"; import { elementIsFillableFormField, - generateRandomCustomElementName, + getAttributeBoolean, sendExtensionMessage, - setElementStyles, + throttle, } from "../utils"; -import { AutofillOverlayElement, RedirectFocusDirection } from "../utils/autofill-overlay.enum"; import { + AutofillOverlayContentExtensionMessageHandlers, AutofillOverlayContentService as AutofillOverlayContentServiceInterface, - OpenAutofillOverlayOptions, + OpenAutofillInlineMenuOptions, + SubFrameDataFromWindowMessage, } from "./abstractions/autofill-overlay-content.service"; +import { InlineMenuFieldQualificationService } from "./abstractions/inline-menu-field-qualifications.service"; import { AutoFillConstants } from "./autofill-constants"; -import { InlineMenuFieldQualificationService } from "./inline-menu-field-qualification.service"; -class AutofillOverlayContentService implements AutofillOverlayContentServiceInterface { - private readonly inlineMenuFieldQualificationService: InlineMenuFieldQualificationService; - isFieldCurrentlyFocused = false; - isCurrentlyFilling = false; - isOverlayCiphersPopulated = false; +export class AutofillOverlayContentService implements AutofillOverlayContentServiceInterface { pageDetailsUpdateRequired = false; - autofillOverlayVisibility: number; - private isFirefoxBrowser = - globalThis.navigator.userAgent.indexOf(" Firefox/") !== -1 || - globalThis.navigator.userAgent.indexOf(" Gecko/") !== -1; - private readonly generateRandomCustomElementName = generateRandomCustomElementName; + inlineMenuVisibility: number; private readonly findTabs = tabbable; private readonly sendExtensionMessage = sendExtensionMessage; private formFieldElements: Set> = new Set([]); - private ignoredFieldTypes: Set = new Set(AutoFillConstants.ExcludedOverlayTypes); + private hiddenFormFieldElements: WeakMap, AutofillField> = + new WeakMap(); + private ignoredFieldTypes: Set = new Set(AutoFillConstants.ExcludedInlineMenuTypes); private userFilledFields: Record = {}; private authStatus: AuthenticationStatus; private focusableElements: FocusableElement[] = []; - private isOverlayButtonVisible = false; - private isOverlayListVisible = false; - private overlayButtonElement: HTMLElement; - private overlayListElement: HTMLElement; private mostRecentlyFocusedField: ElementWithOpId; private focusedFieldData: FocusedFieldData; - private userInteractionEventTimeout: number | NodeJS.Timeout; - private overlayElementsMutationObserver: MutationObserver; - private bodyElementMutationObserver: MutationObserver; - private documentElementMutationObserver: MutationObserver; - private mutationObserverIterations = 0; - private mutationObserverIterationsResetTimeout: number | NodeJS.Timeout; - private autofillFieldKeywordsMap: WeakMap = new WeakMap(); + private closeInlineMenuOnRedirectTimeout: number | NodeJS.Timeout; + private focusInlineMenuListTimeout: number | NodeJS.Timeout; private eventHandlersMemo: { [key: string]: EventListener } = {}; - private readonly customElementDefaultStyles: Partial = { - all: "initial", - position: "fixed", - display: "block", - zIndex: "2147483647", + private readonly extensionMessageHandlers: AutofillOverlayContentExtensionMessageHandlers = { + openAutofillInlineMenu: ({ message }) => this.openInlineMenu(message), + addNewVaultItemFromOverlay: () => this.addNewVaultItem(), + blurMostRecentlyFocusedField: () => this.blurMostRecentlyFocusedField(), + unsetMostRecentlyFocusedField: () => this.unsetMostRecentlyFocusedField(), + checkIsMostRecentlyFocusedFieldWithinViewport: () => + this.checkIsMostRecentlyFocusedFieldWithinViewport(), + bgUnlockPopoutOpened: () => this.blurMostRecentlyFocusedField(true), + bgVaultItemRepromptPopoutOpened: () => this.blurMostRecentlyFocusedField(true), + redirectAutofillInlineMenuFocusOut: ({ message }) => + this.redirectInlineMenuFocusOut(message?.data?.direction), + updateAutofillInlineMenuVisibility: ({ message }) => this.updateInlineMenuVisibility(message), + getSubFrameOffsets: ({ message }) => this.getSubFrameOffsets(message), + getSubFrameOffsetsFromWindowMessage: ({ message }) => + this.getSubFrameOffsetsFromWindowMessage(message), + checkMostRecentlyFocusedFieldHasValue: () => this.mostRecentlyFocusedFieldHasValue(), + setupRebuildSubFrameOffsetsListeners: () => this.setupRebuildSubFrameOffsetsListeners(), + destroyAutofillInlineMenuListeners: () => this.destroy(), }; - constructor() { - this.inlineMenuFieldQualificationService = new InlineMenuFieldQualificationService(); - } + constructor(private inlineMenuFieldQualificationService: InlineMenuFieldQualificationService) {} /** * Initializes the autofill overlay content service by setting up the mutation observers. @@ -83,14 +91,22 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte } /** - * Sets up the autofill overlay listener on the form field element. This method is called + * Getter used to access the extension message handlers associated + * with the autofill overlay content service. + */ + get messageHandlers(): AutofillOverlayContentExtensionMessageHandlers { + return this.extensionMessageHandlers; + } + + /** + * Sets up the autofill inline menu listener on the form field element. This method is called * during the page details collection process. * * @param formFieldElement - Form field elements identified during the page details collection process. * @param autofillFieldData - Autofill field data captured from the form field element. * @param pageDetails - The collected page details from the tab. */ - async setupAutofillOverlayListenerOnField( + async setupInlineMenu( formFieldElement: ElementWithOpId, autofillFieldData: AutofillField, pageDetails: AutofillPageDetails, @@ -102,49 +118,36 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte return; } - this.formFieldElements.add(formFieldElement); - - if (!this.autofillOverlayVisibility) { - await this.getAutofillOverlayVisibility(); - } - - this.setupFormFieldElementEventListeners(formFieldElement); - - if (this.getRootNodeActiveElement(formFieldElement) === formFieldElement) { - await this.triggerFormFieldFocusedAction(formFieldElement); + if (this.isHiddenField(formFieldElement, autofillFieldData)) { return; } - if (!this.mostRecentlyFocusedField) { - await this.updateMostRecentlyFocusedField(formFieldElement); - } + await this.setupInlineMenuOnQualifiedField(formFieldElement); } /** - * Handles opening the autofill overlay. Will conditionally open - * the overlay based on the current autofill overlay visibility setting. - * Allows you to optionally focus the field element when opening the overlay. - * Will also optionally ignore the overlay visibility setting and open the + * Handles opening the autofill inline menu. Will conditionally open + * the inline menu based on the current inline menu visibility setting. + * Allows you to optionally focus the field element when opening the inline menu. + * Will also optionally ignore the inline menu visibility setting and open the * - * @param options - Options for opening the autofill overlay. + * @param options - Options for opening the autofill inline menu. */ - openAutofillOverlay(options: OpenAutofillOverlayOptions = {}) { - const { isFocusingFieldElement, isOpeningFullOverlay, authStatus } = options; + openInlineMenu(options: OpenAutofillInlineMenuOptions = {}) { + const { isFocusingFieldElement, isOpeningFullInlineMenu, authStatus } = options; if (!this.mostRecentlyFocusedField) { return; } if (this.pageDetailsUpdateRequired) { - // 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.sendExtensionMessage("bgCollectPageDetails", { + void this.sendExtensionMessage("bgCollectPageDetails", { sender: "autofillOverlayContentService", }); this.pageDetailsUpdateRequired = false; } if (isFocusingFieldElement && !this.recentlyFocusedFieldIsCurrentlyFocused()) { - this.focusMostRecentOverlayField(); + this.focusMostRecentlyFocusedField(); } if (typeof authStatus !== "undefined") { @@ -152,79 +155,47 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte } if ( - this.autofillOverlayVisibility === AutofillOverlayVisibility.OnButtonClick && - !isOpeningFullOverlay + this.inlineMenuVisibility === AutofillOverlayVisibility.OnButtonClick && + !isOpeningFullInlineMenu ) { - this.updateOverlayButtonPosition(); + this.updateInlineMenuButtonPosition(); return; } - this.updateOverlayElementsPosition(); + this.updateInlineMenuElementsPosition(); } /** * Focuses the most recently focused field element. */ - focusMostRecentOverlayField() { + focusMostRecentlyFocusedField() { this.mostRecentlyFocusedField?.focus(); } /** * Removes focus from the most recently focused field element. */ - blurMostRecentOverlayField() { + blurMostRecentlyFocusedField(isClosingInlineMenu: boolean = false) { this.mostRecentlyFocusedField?.blur(); + + if (isClosingInlineMenu) { + void this.sendExtensionMessage("closeAutofillInlineMenu"); + } } /** - * Removes the autofill overlay from the page. This will initially - * unobserve the body element to ensure the mutation observer no - * longer triggers. + * Sets the most recently focused field within the current frame to a `null` value. */ - removeAutofillOverlay = () => { - this.removeBodyElementObserver(); - this.removeAutofillOverlayButton(); - this.removeAutofillOverlayList(); - }; - - /** - * Removes the overlay button from the DOM if it is currently present. Will - * also remove the overlay reposition event listeners. - */ - removeAutofillOverlayButton() { - if (!this.overlayButtonElement) { - return; - } - - this.overlayButtonElement.remove(); - this.isOverlayButtonVisible = false; - void this.sendExtensionMessage("autofillOverlayElementClosed", { - overlayElement: AutofillOverlayElement.Button, - }); - this.removeOverlayRepositionEventListeners(); - } - - /** - * Removes the overlay list from the DOM if it is currently present. - */ - removeAutofillOverlayList() { - if (!this.overlayListElement) { - return; - } - - this.overlayListElement.remove(); - this.isOverlayListVisible = false; - void this.sendExtensionMessage("autofillOverlayElementClosed", { - overlayElement: AutofillOverlayElement.List, - }); + unsetMostRecentlyFocusedField() { + this.mostRecentlyFocusedField = null; } /** * Formats any found user filled fields for a login cipher and sends a message * to the background script to add a new cipher. */ - addNewVaultItem() { - if (!this.isOverlayListVisible) { + async addNewVaultItem() { + if (!(await this.isInlineMenuListVisible())) { return; } @@ -235,26 +206,27 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte hostname: globalThis.document.location.hostname, }; - // 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.sendExtensionMessage("autofillOverlayAddNewVaultItem", { login }); + void this.sendExtensionMessage("autofillOverlayAddNewVaultItem", { login }); } /** - * Redirects the keyboard focus out of the overlay, selecting the element that is + * Redirects the keyboard focus out of the inline menu, selecting the element that is * either previous or next in the tab order. If the direction is current, the most * recently focused field will be focused. * - * @param direction - The direction to redirect the focus. + * @param direction - The direction to redirect the focus out. */ - redirectOverlayFocusOut(direction: string) { - if (!this.isOverlayListVisible || !this.mostRecentlyFocusedField) { + private async redirectInlineMenuFocusOut(direction?: string) { + if (!direction || !this.mostRecentlyFocusedField || !(await this.isInlineMenuListVisible())) { return; } if (direction === RedirectFocusDirection.Current) { - this.focusMostRecentOverlayField(); - setTimeout(this.removeAutofillOverlay, 100); + this.focusMostRecentlyFocusedField(); + this.closeInlineMenuOnRedirectTimeout = globalThis.setTimeout( + () => void this.sendExtensionMessage("closeAutofillInlineMenu"), + 100, + ); return; } @@ -274,7 +246,7 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte /** * Sets up the event listeners that facilitate interaction with the form field elements. * Will clear any cached form field element handlers that are encountered when setting - * up a form field element to the overlay. + * up a form field element. * * @param formFieldElement - The form field element to set up the event listeners for. */ @@ -299,7 +271,7 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte /** * Removes any cached form field element handlers that are encountered - * when setting up a form field element to present the overlay. + * when setting up a form field element to present the inline menu. * * @param formFieldElement - The form field element to remove the cached handlers for. */ @@ -343,33 +315,35 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte /** * Form Field blur event handler. Updates the value identifying whether - * the field is focused and sends a message to check if the overlay itself + * the field is focused and sends a message to check if the inline menu itself * is currently focused. */ private handleFormFieldBlurEvent = () => { - this.isFieldCurrentlyFocused = false; - // 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.sendExtensionMessage("checkAutofillOverlayFocused"); + void this.sendExtensionMessage("updateIsFieldCurrentlyFocused", { + isFieldCurrentlyFocused: false, + }); + void this.sendExtensionMessage("checkAutofillInlineMenuFocused"); }; /** * Form field keyup event handler. Facilitates the ability to remove the - * autofill overlay using the escape key, focusing the overlay list using - * the ArrowDown key, and ensuring that the overlay is repositioned when + * autofill inline menu using the escape key, focusing the inline menu list using + * the ArrowDown key, and ensuring that the inline menu is repositioned when * the form is submitted using the Enter key. * * @param event - The keyup event. */ - private handleFormFieldKeyupEvent = (event: KeyboardEvent) => { + private handleFormFieldKeyupEvent = async (event: KeyboardEvent) => { const eventCode = event.code; if (eventCode === "Escape") { - this.removeAutofillOverlay(); + void this.sendExtensionMessage("closeAutofillInlineMenu", { + forceCloseInlineMenu: true, + }); return; } - if (eventCode === "Enter" && !this.isCurrentlyFilling) { - this.handleOverlayRepositionEvent(); + if (eventCode === "Enter" && !(await this.isFieldCurrentlyFilling())) { + void this.handleOverlayRepositionEvent(); return; } @@ -377,28 +351,28 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte event.preventDefault(); event.stopPropagation(); - // 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.focusOverlayList(); + void this.focusInlineMenuList(); } }; /** - * Triggers a focus of the overlay list, if it is visible. If the list is not visible, - * the overlay will be opened and the list will be focused after a short delay. Ensures - * that the overlay list is focused when the user presses the down arrow key. + * Triggers a focus of the inline menu list, if it is visible. If the list is not visible, + * the inline menu will be opened and the list will be focused after a short delay. Ensures + * that the inline menu list is focused when the user presses the down arrow key. */ - private async focusOverlayList() { - if (!this.isOverlayListVisible && this.mostRecentlyFocusedField) { + private async focusInlineMenuList() { + if (this.mostRecentlyFocusedField && !(await this.isInlineMenuListVisible())) { + this.clearFocusInlineMenuListTimeout(); await this.updateMostRecentlyFocusedField(this.mostRecentlyFocusedField); - this.openAutofillOverlay({ isOpeningFullOverlay: true }); - setTimeout(() => this.sendExtensionMessage("focusAutofillOverlayList"), 125); + this.openInlineMenu({ isOpeningFullInlineMenu: true }); + this.focusInlineMenuListTimeout = globalThis.setTimeout( + () => this.sendExtensionMessage("focusAutofillInlineMenuList"), + 125, + ); return; } - // 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.sendExtensionMessage("focusAutofillOverlayList"); + void this.sendExtensionMessage("focusAutofillInlineMenuList"); } /** @@ -416,23 +390,26 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte /** * Triggers when the form field element receives an input event. This method will * store the modified form element data for use when the user attempts to add a new - * vault item. It also acts to remove the overlay list while the user is typing. + * vault item. It also acts to remove the inline menu list while the user is typing. * * @param formFieldElement - The form field element that triggered the input event. */ - private triggerFormFieldInput(formFieldElement: ElementWithOpId) { + private async triggerFormFieldInput(formFieldElement: ElementWithOpId) { if (!elementIsFillableFormField(formFieldElement)) { return; } this.storeModifiedFormElement(formFieldElement); - if (formFieldElement.value && (this.isOverlayCiphersPopulated || !this.isUserAuthed())) { - this.removeAutofillOverlayList(); + if (await this.hideInlineMenuListOnFilledField(formFieldElement)) { + void this.sendExtensionMessage("closeAutofillInlineMenu", { + overlayElement: AutofillOverlayElement.List, + forceCloseInlineMenu: true, + }); return; } - this.openAutofillOverlay(); + this.openInlineMenu(); } /** @@ -444,8 +421,8 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte * @private */ private storeModifiedFormElement(formFieldElement: ElementWithOpId) { - if (formFieldElement === this.mostRecentlyFocusedField) { - this.mostRecentlyFocusedField = formFieldElement; + if (formFieldElement !== this.mostRecentlyFocusedField) { + void this.updateMostRecentlyFocusedField(formFieldElement); } if (formFieldElement.type === "password") { @@ -470,12 +447,12 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte /** * Triggers when the form field element receives a click event. This method will - * trigger the focused action for the form field element if the overlay is not visible. + * trigger the focused action for the form field element if the inline menu is not visible. * * @param formFieldElement - The form field element that triggered the click event. */ private async triggerFormFieldClickedAction(formFieldElement: ElementWithOpId) { - if (this.isOverlayButtonVisible || this.isOverlayListVisible) { + if ((await this.isInlineMenuButtonVisible()) || (await this.isInlineMenuListVisible())) { return; } @@ -496,37 +473,39 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte /** * Triggers when the form field element receives a focus event. This method will - * update the most recently focused field and open the autofill overlay if the + * update the most recently focused field and open the autofill inline menu if the * autofill process is not currently active. * * @param formFieldElement - The form field element that triggered the focus event. */ private async triggerFormFieldFocusedAction(formFieldElement: ElementWithOpId) { - if (this.isCurrentlyFilling) { + if (await this.isFieldCurrentlyFilling()) { return; } - this.isFieldCurrentlyFocused = true; - this.clearUserInteractionEventTimeout(); + await this.sendExtensionMessage("updateIsFieldCurrentlyFocused", { + isFieldCurrentlyFocused: true, + }); const initiallyFocusedField = this.mostRecentlyFocusedField; await this.updateMostRecentlyFocusedField(formFieldElement); - const formElementHasValue = Boolean((formFieldElement as HTMLInputElement).value); if ( - this.autofillOverlayVisibility === AutofillOverlayVisibility.OnButtonClick || - (formElementHasValue && initiallyFocusedField !== this.mostRecentlyFocusedField) + this.inlineMenuVisibility === AutofillOverlayVisibility.OnButtonClick || + (initiallyFocusedField !== this.mostRecentlyFocusedField && + (await this.hideInlineMenuListOnFilledField(formFieldElement as FillableFormFieldElement))) ) { - this.removeAutofillOverlayList(); + await this.sendExtensionMessage("closeAutofillInlineMenu", { + overlayElement: AutofillOverlayElement.List, + forceCloseInlineMenu: true, + }); } - if (!formElementHasValue || (!this.isOverlayCiphersPopulated && this.isUserAuthed())) { - // 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.sendExtensionMessage("openAutofillOverlay"); + if (await this.hideInlineMenuListOnFilledField(formFieldElement as FillableFormFieldElement)) { + this.updateInlineMenuButtonPosition(); return; } - this.updateOverlayButtonPosition(); + void this.sendExtensionMessage("openAutofillInlineMenu"); } /** @@ -547,82 +526,33 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte } /** - * Updates the position of both the overlay button and overlay list. + * Updates the position of both the inline menu button and list. */ - private updateOverlayElementsPosition() { - this.updateOverlayButtonPosition(); - this.updateOverlayListPosition(); + private updateInlineMenuElementsPosition() { + this.updateInlineMenuButtonPosition(); + this.updateInlineMenuListPosition(); } /** - * Updates the position of the overlay button. + * Updates the position of the inline menu button. */ - private updateOverlayButtonPosition() { - if (!this.overlayButtonElement) { - this.createAutofillOverlayButton(); - this.updateCustomElementDefaultStyles(this.overlayButtonElement); - } - - if (!this.isOverlayButtonVisible) { - this.appendOverlayElementToBody(this.overlayButtonElement); - this.isOverlayButtonVisible = true; - this.setOverlayRepositionEventListeners(); - } - // 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.sendExtensionMessage("updateAutofillOverlayPosition", { + private updateInlineMenuButtonPosition() { + void this.sendExtensionMessage("updateAutofillInlineMenuPosition", { overlayElement: AutofillOverlayElement.Button, }); } /** - * Updates the position of the overlay list. + * Updates the position of the inline menu list. */ - private updateOverlayListPosition() { - if (!this.overlayListElement) { - this.createAutofillOverlayList(); - this.updateCustomElementDefaultStyles(this.overlayListElement); - } - - if (!this.isOverlayListVisible) { - this.appendOverlayElementToBody(this.overlayListElement); - this.isOverlayListVisible = true; - } - - // 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.sendExtensionMessage("updateAutofillOverlayPosition", { + private updateInlineMenuListPosition() { + void this.sendExtensionMessage("updateAutofillInlineMenuPosition", { overlayElement: AutofillOverlayElement.List, }); } /** - * Appends the overlay element to the body element. This method will also - * observe the body element to ensure that the overlay element is not - * interfered with by any DOM changes. - * - * @param element - The overlay element to append to the body element. - */ - private appendOverlayElementToBody(element: HTMLElement) { - this.observeBodyElement(); - globalThis.document.body.appendChild(element); - } - - /** - * Sends a message that facilitates hiding the overlay elements. - * - * @param isHidden - Indicates if the overlay elements should be hidden. - */ - private toggleOverlayHidden(isHidden: boolean) { - const displayValue = isHidden ? "none" : "block"; - void this.sendExtensionMessage("updateAutofillOverlayHidden", { display: displayValue }); - - this.isOverlayButtonVisible = !!this.overlayButtonElement && !isHidden; - this.isOverlayListVisible = !!this.overlayListElement && !isHidden; - } - - /** - * Updates the data used to position the overlay elements in relation + * Updates the data used to position the inline menu elements in relation * to the most recently focused form field. * * @param formFieldElement - The form field element that triggered the focus event. @@ -630,6 +560,10 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte private async updateMostRecentlyFocusedField( formFieldElement: ElementWithOpId, ) { + if (!formFieldElement || !elementIsFillableFormField(formFieldElement)) { + return; + } + this.mostRecentlyFocusedField = formFieldElement; const { paddingRight, paddingLeft } = globalThis.getComputedStyle(formFieldElement); const { width, height, top, left } = @@ -639,9 +573,7 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte focusedFieldRects: { width, height, top, left }, }; - // 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.sendExtensionMessage("updateFocusedFieldData", { + await this.sendExtensionMessage("updateFocusedFieldData", { focusedFieldData: this.focusedFieldData, }); } @@ -701,7 +633,7 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte } /** - * Identifies if the field should have the autofill overlay setup on it. Currently, this is mainly + * Identifies if the field should have the autofill inline menu setup on it. Currently, this is mainly * determined by whether the field correlates with a login cipher. This method will need to be * updated in the future to support other types of forms. * @@ -712,12 +644,7 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte autofillFieldData: AutofillField, pageDetails: AutofillPageDetails, ): boolean { - if ( - autofillFieldData.readonly || - autofillFieldData.disabled || - !autofillFieldData.viewable || - this.ignoredFieldTypes.has(autofillFieldData.type) - ) { + if (this.ignoredFieldTypes.has(autofillFieldData.type)) { return true; } @@ -728,354 +655,167 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte } /** - * Creates the autofill overlay button element. Will not attempt - * to create the element if it already exists in the DOM. - */ - private createAutofillOverlayButton() { - if (this.overlayButtonElement) { - return; - } - - if (this.isFirefoxBrowser) { - this.overlayButtonElement = globalThis.document.createElement("div"); - new AutofillOverlayButtonIframe(this.overlayButtonElement); - - return; - } - - const customElementName = this.generateRandomCustomElementName(); - globalThis.customElements?.define( - customElementName, - class extends HTMLElement { - constructor() { - super(); - new AutofillOverlayButtonIframe(this); - } - }, - ); - this.overlayButtonElement = globalThis.document.createElement(customElementName); - } - - /** - * Creates the autofill overlay list element. Will not attempt - * to create the element if it already exists in the DOM. - */ - private createAutofillOverlayList() { - if (this.overlayListElement) { - return; - } - - if (this.isFirefoxBrowser) { - this.overlayListElement = globalThis.document.createElement("div"); - new AutofillOverlayListIframe(this.overlayListElement); - - return; - } - - const customElementName = this.generateRandomCustomElementName(); - globalThis.customElements?.define( - customElementName, - class extends HTMLElement { - constructor() { - super(); - new AutofillOverlayListIframe(this); - } - }, - ); - this.overlayListElement = globalThis.document.createElement(customElementName); - } - - /** - * Updates the default styles for the custom element. This method will - * remove any styles that are added to the custom element by other methods. + * Validates whether a field is considered to be "hidden" based on the field's attributes. + * If the field is hidden, a fallback listener will be set up to ensure that the + * field will have the inline menu set up on it when it becomes visible. * - * @param element - The custom element to update the default styles for. + * @param formFieldElement - The form field element that triggered the focus event. + * @param autofillFieldData - Autofill field data captured from the form field element. */ - private updateCustomElementDefaultStyles(element: HTMLElement) { - this.unobserveCustomElements(); + private isHiddenField( + formFieldElement: ElementWithOpId, + autofillFieldData: AutofillField, + ): boolean { + if (!autofillFieldData.readonly && !autofillFieldData.disabled && autofillFieldData.viewable) { + this.removeHiddenFieldFallbackListener(formFieldElement); + return false; + } - setElementStyles(element, this.customElementDefaultStyles, true); - - this.observeCustomElements(); + this.setupHiddenFieldFallbackListener(formFieldElement, autofillFieldData); + return true; } /** - * Queries the background script for the autofill overlay visibility setting. + * Sets up a fallback listener that will facilitate setting up the + * inline menu on the field when it becomes visible and focused. + * + * @param formFieldElement - The form field element that triggered the focus event. + * @param autofillFieldData - Autofill field data captured from the form field element. + */ + private setupHiddenFieldFallbackListener( + formFieldElement: ElementWithOpId, + autofillFieldData: AutofillField, + ) { + this.hiddenFormFieldElements.set(formFieldElement, autofillFieldData); + formFieldElement.addEventListener(EVENTS.FOCUS, this.handleHiddenFieldFocusEvent); + } + + /** + * Removes the fallback listener that facilitates setting up the inline + * menu on the field when it becomes visible and focused. + * + * @param formFieldElement - The form field element that triggered the focus event. + */ + private removeHiddenFieldFallbackListener(formFieldElement: ElementWithOpId) { + formFieldElement.removeEventListener(EVENTS.FOCUS, this.handleHiddenFieldFocusEvent); + this.hiddenFormFieldElements.delete(formFieldElement); + } + + /** + * Handles the focus event on a hidden field. When + * triggered, the inline menu is set up on the field. + * + * @param event - The focus event. + */ + private handleHiddenFieldFocusEvent = (event: FocusEvent) => { + const formFieldElement = event.target as ElementWithOpId; + const autofillFieldData = this.hiddenFormFieldElements.get(formFieldElement); + if (autofillFieldData) { + autofillFieldData.readonly = getAttributeBoolean(formFieldElement, "disabled"); + autofillFieldData.disabled = getAttributeBoolean(formFieldElement, "disabled"); + autofillFieldData.viewable = true; + void this.setupInlineMenuOnQualifiedField(formFieldElement); + } + + this.removeHiddenFieldFallbackListener(formFieldElement); + }; + + /** + * Sets up the inline menu on a qualified form field element. + * + * @param formFieldElement - The form field element to set up the inline menu on. + */ + private async setupInlineMenuOnQualifiedField( + formFieldElement: ElementWithOpId, + ) { + this.formFieldElements.add(formFieldElement); + + if (!this.mostRecentlyFocusedField) { + await this.updateMostRecentlyFocusedField(formFieldElement); + } + + if (!this.inlineMenuVisibility) { + await this.getInlineMenuVisibility(); + } + + this.setupFormFieldElementEventListeners(formFieldElement); + + if ( + globalThis.document.hasFocus() && + this.getRootNodeActiveElement(formFieldElement) === formFieldElement + ) { + await this.triggerFormFieldFocusedAction(formFieldElement); + } + } + + /** + * Queries the background script for the autofill inline menu visibility setting. * If the setting is not found, a default value of OnFieldFocus will be used * @private */ - private async getAutofillOverlayVisibility() { - const overlayVisibility = await this.sendExtensionMessage("getAutofillOverlayVisibility"); - this.autofillOverlayVisibility = overlayVisibility || AutofillOverlayVisibility.OnFieldFocus; + private async getInlineMenuVisibility() { + const inlineMenuVisibility = await this.sendExtensionMessage("getAutofillInlineMenuVisibility"); + this.inlineMenuVisibility = inlineMenuVisibility || AutofillOverlayVisibility.OnFieldFocus; } /** - * Sets up event listeners that facilitate repositioning - * the autofill overlay on scroll or resize. - */ - private setOverlayRepositionEventListeners() { - globalThis.addEventListener(EVENTS.SCROLL, this.handleOverlayRepositionEvent, { - capture: true, - }); - globalThis.addEventListener(EVENTS.RESIZE, this.handleOverlayRepositionEvent); - } - - /** - * Removes the listeners that facilitate repositioning - * the autofill overlay on scroll or resize. - */ - private removeOverlayRepositionEventListeners() { - globalThis.removeEventListener(EVENTS.SCROLL, this.handleOverlayRepositionEvent, { - capture: true, - }); - globalThis.removeEventListener(EVENTS.RESIZE, this.handleOverlayRepositionEvent); - } - - /** - * Handles the resize or scroll events that enact - * repositioning of the overlay. - */ - private handleOverlayRepositionEvent = () => { - if (!this.isOverlayButtonVisible && !this.isOverlayListVisible) { - return; - } - - this.toggleOverlayHidden(true); - this.clearUserInteractionEventTimeout(); - this.userInteractionEventTimeout = setTimeout( - this.triggerOverlayRepositionUpdates, - 750, - ) as unknown as number; - }; - - /** - * Triggers the overlay reposition updates. This method ensures that the overlay elements - * are correctly positioned when the viewport scrolls or repositions. - */ - private triggerOverlayRepositionUpdates = async () => { - if (!this.recentlyFocusedFieldIsCurrentlyFocused()) { - this.toggleOverlayHidden(false); - this.removeAutofillOverlay(); - return; - } - - await this.updateMostRecentlyFocusedField(this.mostRecentlyFocusedField); - this.updateOverlayElementsPosition(); - this.toggleOverlayHidden(false); - this.clearUserInteractionEventTimeout(); - - if ( - this.focusedFieldData.focusedFieldRects?.top > 0 && - this.focusedFieldData.focusedFieldRects?.top < globalThis.innerHeight - ) { - return; - } - - this.removeAutofillOverlay(); - }; - - /** - * Clears the user interaction event timeout. This is used to ensure that - * the overlay is not repositioned while the user is interacting with it. - */ - private clearUserInteractionEventTimeout() { - if (this.userInteractionEventTimeout) { - clearTimeout(this.userInteractionEventTimeout); - } - } - - /** - * Sets up global event listeners and the mutation - * observer to facilitate required changes to the - * overlay elements. - */ - private setupGlobalEventListeners = () => { - globalThis.document.addEventListener(EVENTS.VISIBILITYCHANGE, this.handleVisibilityChangeEvent); - globalThis.addEventListener(EVENTS.FOCUSOUT, this.handleFormFieldBlurEvent); - this.setupMutationObserver(); - }; - - /** - * Handles the visibility change event. This method will remove the - * autofill overlay if the document is not visible. - */ - private handleVisibilityChangeEvent = () => { - if (document.visibilityState === "visible") { - return; - } - - this.mostRecentlyFocusedField = null; - this.removeAutofillOverlay(); - }; - - /** - * Sets up mutation observers for the overlay elements, the body element, and the - * document element. The mutation observers are used to remove any styles that are - * added to the overlay elements by the website. They are also used to ensure that - * the overlay elements are always present at the bottom of the body element. - */ - private setupMutationObserver = () => { - this.overlayElementsMutationObserver = new MutationObserver( - this.handleOverlayElementMutationObserverUpdate, - ); - - this.bodyElementMutationObserver = new MutationObserver( - this.handleBodyElementMutationObserverUpdate, - ); - }; - - /** - * Sets up mutation observers to verify that the overlay - * elements are not modified by the website. - */ - private observeCustomElements() { - if (this.overlayButtonElement) { - this.overlayElementsMutationObserver?.observe(this.overlayButtonElement, { - attributes: true, - }); - } - - if (this.overlayListElement) { - this.overlayElementsMutationObserver?.observe(this.overlayListElement, { attributes: true }); - } - } - - /** - * Disconnects the mutation observers that are used to verify that the overlay - * elements are not modified by the website. - */ - private unobserveCustomElements() { - this.overlayElementsMutationObserver?.disconnect(); - } - - /** - * Sets up a mutation observer for the body element. The mutation observer is used - * to ensure that the overlay elements are always present at the bottom of the body - * element. - */ - private observeBodyElement() { - this.bodyElementMutationObserver?.observe(globalThis.document.body, { childList: true }); - } - - /** - * Disconnects the mutation observer for the body element. - */ - private removeBodyElementObserver() { - this.bodyElementMutationObserver?.disconnect(); - } - - /** - * Handles the mutation observer update for the overlay elements. This method will - * remove any attributes or styles that might be added to the overlay elements by - * a separate process within the website where this script is injected. + * Returns a value that indicates if we should hide the inline menu list due to a filled field. * - * @param mutationRecord - The mutation record that triggered the update. + * @param formFieldElement - The form field element that triggered the focus event. */ - private handleOverlayElementMutationObserverUpdate = (mutationRecord: MutationRecord[]) => { - if (this.isTriggeringExcessiveMutationObserverIterations()) { - return; - } - - for (let recordIndex = 0; recordIndex < mutationRecord.length; recordIndex++) { - const record = mutationRecord[recordIndex]; - if (record.type !== "attributes") { - continue; - } - - const element = record.target as HTMLElement; - if (record.attributeName !== "style") { - this.removeModifiedElementAttributes(element); - - continue; - } - - element.removeAttribute("style"); - this.updateCustomElementDefaultStyles(element); - } - }; + private async hideInlineMenuListOnFilledField( + formFieldElement?: FillableFormFieldElement, + ): Promise { + return ( + formFieldElement?.value && + ((await this.isInlineMenuCiphersPopulated()) || !this.isUserAuthed()) + ); + } /** - * Removes all elements from a passed overlay - * element except for the style attribute. - * - * @param element - The element to remove the attributes from. + * Indicates whether the most recently focused field has a value. */ - private removeModifiedElementAttributes(element: HTMLElement) { - const attributes = Array.from(element.attributes); - for (let attributeIndex = 0; attributeIndex < attributes.length; attributeIndex++) { - const attribute = attributes[attributeIndex]; - if (attribute.name === "style") { - continue; - } + private mostRecentlyFocusedFieldHasValue() { + return Boolean((this.mostRecentlyFocusedField as FillableFormFieldElement)?.value); + } - element.removeAttribute(attribute.name); + /** + * Updates the local reference to the inline menu visibility setting. + * + * @param data - The data object from the extension message. + */ + private updateInlineMenuVisibility({ data }: AutofillExtensionMessage) { + if (!isNaN(data?.inlineMenuVisibility)) { + this.inlineMenuVisibility = data.inlineMenuVisibility; } } /** - * Handles the mutation observer update for the body element. This method will - * ensure that the overlay elements are always present at the bottom of the body - * element. + * Checks if a field is currently filling within an frame in the tab. */ - private handleBodyElementMutationObserverUpdate = () => { - if ( - (!this.overlayButtonElement && !this.overlayListElement) || - this.isTriggeringExcessiveMutationObserverIterations() - ) { - return; - } - - const lastChild = globalThis.document.body.lastElementChild; - const secondToLastChild = lastChild?.previousElementSibling; - const lastChildIsOverlayList = lastChild === this.overlayListElement; - const lastChildIsOverlayButton = lastChild === this.overlayButtonElement; - const secondToLastChildIsOverlayButton = secondToLastChild === this.overlayButtonElement; - - if ( - (lastChildIsOverlayList && secondToLastChildIsOverlayButton) || - (lastChildIsOverlayButton && !this.isOverlayListVisible) - ) { - return; - } - - if ( - (lastChildIsOverlayList && !secondToLastChildIsOverlayButton) || - (lastChildIsOverlayButton && this.isOverlayListVisible) - ) { - globalThis.document.body.insertBefore(this.overlayButtonElement, this.overlayListElement); - return; - } - - globalThis.document.body.insertBefore(lastChild, this.overlayButtonElement); - }; + private async isFieldCurrentlyFilling() { + return (await this.sendExtensionMessage("checkIsFieldCurrentlyFilling")) === true; + } /** - * Identifies if the mutation observer is triggering excessive iterations. - * Will trigger a blur of the most recently focused field and remove the - * autofill overlay if any set mutation observer is triggering - * excessive iterations. + * Checks if the inline menu button is visible at the top frame. */ - private isTriggeringExcessiveMutationObserverIterations() { - if (this.mutationObserverIterationsResetTimeout) { - clearTimeout(this.mutationObserverIterationsResetTimeout); - } + private async isInlineMenuButtonVisible() { + return (await this.sendExtensionMessage("checkIsAutofillInlineMenuButtonVisible")) === true; + } - this.mutationObserverIterations++; - this.mutationObserverIterationsResetTimeout = setTimeout( - () => (this.mutationObserverIterations = 0), - 2000, - ); + /** + * Checks if the inline menu list if visible at the top frame. + */ + private async isInlineMenuListVisible() { + return (await this.sendExtensionMessage("checkIsAutofillInlineMenuListVisible")) === true; + } - if (this.mutationObserverIterations > 100) { - clearTimeout(this.mutationObserverIterationsResetTimeout); - this.mutationObserverIterations = 0; - this.blurMostRecentOverlayField(); - this.removeAutofillOverlay(); - - return true; - } - - return false; + /** + * Checks if the current tab contains ciphers that can be used to populate the inline menu. + */ + private async isInlineMenuCiphersPopulated() { + return (await this.sendExtensionMessage("checkIsInlineMenuCiphersPopulated")) === true; } /** @@ -1084,31 +824,394 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte * @param element - The element to get the root node active element for. */ private getRootNodeActiveElement(element: Element): Element { + if (!element) { + return null; + } + const documentRoot = element.getRootNode() as ShadowRoot | Document; return documentRoot?.activeElement; } + /** + * Queries all iframe elements within the document and returns the + * sub frame offsets for each iframe element. + * + * @param message - The message object from the extension. + */ + private async getSubFrameOffsets( + message: AutofillExtensionMessage, + ): Promise { + const { subFrameUrl } = message; + + const subFrameUrlVariations = this.getSubFrameUrlVariations(subFrameUrl); + if (!subFrameUrlVariations) { + return null; + } + + let iframeElement: HTMLIFrameElement | null = null; + const iframeElements = globalThis.document.getElementsByTagName("iframe"); + + for (let iframeIndex = 0; iframeIndex < iframeElements.length; iframeIndex++) { + const iframe = iframeElements[iframeIndex]; + if (!subFrameUrlVariations.has(iframe.src)) { + continue; + } + + if (iframeElement) { + return null; + } + + iframeElement = iframe; + } + + if (!iframeElement) { + return null; + } + + return this.calculateSubFrameOffsets(iframeElement, subFrameUrl); + } + + /** + * Returns a set of all possible URL variations for the sub frame URL. + * + * @param subFrameUrl - The URL of the sub frame. + */ + private getSubFrameUrlVariations(subFrameUrl: string) { + try { + const url = new URL(subFrameUrl, globalThis.location.href); + const pathAndHash = url.pathname + url.hash; + const pathAndSearch = url.pathname + url.search; + const pathSearchAndHash = pathAndSearch + url.hash; + const pathNameWithoutTrailingSlash = url.pathname.replace(/\/$/, ""); + const pathWithoutTrailingSlashAndHash = pathNameWithoutTrailingSlash + url.hash; + const pathWithoutTrailingSlashAndSearch = pathNameWithoutTrailingSlash + url.search; + const pathWithoutTrailingSlashSearchAndHash = pathWithoutTrailingSlashAndSearch + url.hash; + + return new Set([ + url.href, + url.href.replace(/\/$/, ""), + url.pathname, + pathAndHash, + pathAndSearch, + pathSearchAndHash, + pathNameWithoutTrailingSlash, + pathWithoutTrailingSlashAndHash, + pathWithoutTrailingSlashAndSearch, + pathWithoutTrailingSlashSearchAndHash, + url.hostname + url.pathname, + url.hostname + pathAndHash, + url.hostname + pathAndSearch, + url.hostname + pathSearchAndHash, + url.hostname + pathNameWithoutTrailingSlash, + url.hostname + pathWithoutTrailingSlashAndHash, + url.hostname + pathWithoutTrailingSlashAndSearch, + url.hostname + pathWithoutTrailingSlashSearchAndHash, + url.origin + url.pathname, + url.origin + pathAndHash, + url.origin + pathAndSearch, + url.origin + pathSearchAndHash, + url.origin + pathNameWithoutTrailingSlash, + url.origin + pathWithoutTrailingSlashAndHash, + url.origin + pathWithoutTrailingSlashAndSearch, + url.origin + pathWithoutTrailingSlashSearchAndHash, + ]); + } catch (_error) { + return null; + } + } + + /** + * Posts a message to the parent frame to calculate the sub frame offset of the current frame. + * + * @param message - The message object from the extension. + */ + private getSubFrameOffsetsFromWindowMessage(message: any) { + globalThis.parent.postMessage( + { + command: "calculateSubFramePositioning", + subFrameData: { + url: window.location.href, + frameId: message.subFrameId, + left: 0, + top: 0, + parentFrameIds: [0], + subFrameDepth: 0, + } as SubFrameDataFromWindowMessage, + }, + "*", + ); + } + + /** + * Calculates the bounding rect for the queried frame and returns the + * offset data for the sub frame. + * + * @param iframeElement - The iframe element to calculate the sub frame offsets for. + * @param subFrameUrl - The URL of the sub frame. + * @param frameId - The frame ID of the sub frame. + */ + private calculateSubFrameOffsets( + iframeElement: HTMLIFrameElement, + subFrameUrl?: string, + frameId?: number, + ): SubFrameOffsetData { + const iframeRect = iframeElement.getBoundingClientRect(); + const iframeStyles = globalThis.getComputedStyle(iframeElement); + const paddingLeft = parseInt(iframeStyles.getPropertyValue("padding-left")) || 0; + const paddingTop = parseInt(iframeStyles.getPropertyValue("padding-top")) || 0; + const borderWidthLeft = parseInt(iframeStyles.getPropertyValue("border-left-width")) || 0; + const borderWidthTop = parseInt(iframeStyles.getPropertyValue("border-top-width")) || 0; + + return { + url: subFrameUrl, + frameId, + top: iframeRect.top + paddingTop + borderWidthTop, + left: iframeRect.left + paddingLeft + borderWidthLeft, + }; + } + + /** + * Calculates the sub frame positioning for the current frame + * through all parent frames until the top frame is reached. + * + * @param event - The message event. + */ + private calculateSubFramePositioning = async (event: MessageEvent) => { + const subFrameData: SubFrameDataFromWindowMessage = event.data.subFrameData; + + subFrameData.subFrameDepth++; + if (subFrameData.subFrameDepth >= MAX_SUB_FRAME_DEPTH) { + void this.sendExtensionMessage("destroyAutofillInlineMenuListeners", { subFrameData }); + return; + } + + let subFrameOffsets: SubFrameOffsetData; + const iframes = globalThis.document.querySelectorAll("iframe"); + for (let i = 0; i < iframes.length; i++) { + if (iframes[i].contentWindow === event.source) { + const iframeElement = iframes[i]; + subFrameOffsets = this.calculateSubFrameOffsets( + iframeElement, + subFrameData.url, + subFrameData.frameId, + ); + + subFrameData.top += subFrameOffsets.top; + subFrameData.left += subFrameOffsets.left; + + const parentFrameId = await this.sendExtensionMessage("getCurrentTabFrameId"); + if (typeof parentFrameId !== "undefined") { + subFrameData.parentFrameIds.push(parentFrameId); + } + + break; + } + } + + if (globalThis.window.self !== globalThis.window.top) { + globalThis.parent.postMessage({ command: "calculateSubFramePositioning", subFrameData }, "*"); + return; + } + + void this.sendExtensionMessage("updateSubFrameData", { subFrameData }); + }; + + /** + * Sets up global event listeners and the mutation + * observer to facilitate required changes to the + * overlay elements. + */ + private setupGlobalEventListeners = () => { + globalThis.addEventListener(EVENTS.MESSAGE, this.handleWindowMessageEvent); + globalThis.document.addEventListener(EVENTS.VISIBILITYCHANGE, this.handleVisibilityChangeEvent); + globalThis.addEventListener(EVENTS.FOCUSOUT, this.handleFormFieldBlurEvent); + this.setOverlayRepositionEventListeners(); + }; + + /** + * Handles window messages that are sent to the current frame. Will trigger a + * calculation of the sub frame offsets through the parent frame. + * + * @param event - The message event. + */ + private handleWindowMessageEvent = (event: MessageEvent) => { + if (event.data?.command === "calculateSubFramePositioning") { + void this.calculateSubFramePositioning(event); + } + }; + + /** + * Handles the visibility change event. This method will remove the + * autofill overlay if the document is not visible. + */ + private handleVisibilityChangeEvent = () => { + if (!this.mostRecentlyFocusedField || globalThis.document.visibilityState === "visible") { + return; + } + + this.unsetMostRecentlyFocusedField(); + void this.sendExtensionMessage("closeAutofillInlineMenu", { + forceCloseInlineMenu: true, + }); + }; + + /** + * Sets up event listeners that facilitate repositioning + * the overlay elements on scroll or resize. + */ + private setOverlayRepositionEventListeners() { + const handler = this.useEventHandlersMemo( + throttle(this.handleOverlayRepositionEvent, 250), + AUTOFILL_OVERLAY_HANDLE_REPOSITION, + ); + globalThis.addEventListener(EVENTS.SCROLL, handler, { + capture: true, + passive: true, + }); + globalThis.addEventListener(EVENTS.RESIZE, handler); + } + + /** + * Removes the listeners that facilitate repositioning + * the overlay elements on scroll or resize. + */ + private removeOverlayRepositionEventListeners() { + const handler = this.eventHandlersMemo[AUTOFILL_OVERLAY_HANDLE_REPOSITION]; + globalThis.removeEventListener(EVENTS.SCROLL, handler, { + capture: true, + }); + globalThis.removeEventListener(EVENTS.RESIZE, handler); + + delete this.eventHandlersMemo[AUTOFILL_OVERLAY_HANDLE_REPOSITION]; + } + + /** + * Handles the resize or scroll events that enact + * repositioning of existing overlay elements. + */ + private handleOverlayRepositionEvent = async () => { + await this.sendExtensionMessage("triggerAutofillOverlayReposition"); + }; + + /** + * Sets up listeners that facilitate a rebuild of the sub frame offsets + * when a user interacts or focuses an element within the frame. + */ + private setupRebuildSubFrameOffsetsListeners = () => { + if (globalThis.window.top === globalThis.window || this.formFieldElements.size < 1) { + return; + } + this.removeSubFrameFocusOutListeners(); + + globalThis.addEventListener(EVENTS.FOCUS, this.handleSubFrameFocusInEvent); + globalThis.document.body.addEventListener(EVENTS.MOUSEENTER, this.handleSubFrameFocusInEvent); + }; + + /** + * Removes the listeners that facilitate a rebuild of the sub frame offsets. + */ + private removeRebuildSubFrameOffsetsListeners = () => { + globalThis.removeEventListener(EVENTS.FOCUS, this.handleSubFrameFocusInEvent); + globalThis.document.body.removeEventListener( + EVENTS.MOUSEENTER, + this.handleSubFrameFocusInEvent, + ); + }; + + /** + * Re-establishes listeners that handle the sub frame offsets rebuild of the frame + * based on user interaction with the sub frame. + */ + private setupSubFrameFocusOutListeners = () => { + globalThis.addEventListener(EVENTS.BLUR, this.setupRebuildSubFrameOffsetsListeners); + globalThis.document.body.addEventListener( + EVENTS.MOUSELEAVE, + this.setupRebuildSubFrameOffsetsListeners, + ); + }; + + /** + * Removes the listeners that trigger when a user focuses away from the sub frame. + */ + private removeSubFrameFocusOutListeners = () => { + globalThis.removeEventListener(EVENTS.BLUR, this.setupRebuildSubFrameOffsetsListeners); + globalThis.document.body.removeEventListener( + EVENTS.MOUSELEAVE, + this.setupRebuildSubFrameOffsetsListeners, + ); + }; + + /** + * Sends a message to the background script to trigger a rebuild of the sub frame + * offsets. Will deregister the listeners to ensure that other focus and mouse + * events do not unnecessarily re-trigger a sub frame rebuild. + */ + private handleSubFrameFocusInEvent = () => { + void this.sendExtensionMessage("triggerSubFrameFocusInRebuild"); + + this.removeRebuildSubFrameOffsetsListeners(); + this.setupSubFrameFocusOutListeners(); + }; + + /** + * Triggers an update in the most recently focused field's data and returns + * whether the field is within the viewport bounds. If not within the bounds + * of the viewport, the inline menu will be closed. + */ + private async checkIsMostRecentlyFocusedFieldWithinViewport() { + await this.updateMostRecentlyFocusedField(this.mostRecentlyFocusedField); + + const focusedFieldRectsTop = this.focusedFieldData?.focusedFieldRects?.top; + const focusedFieldRectsBottom = + focusedFieldRectsTop + this.focusedFieldData?.focusedFieldRects?.height; + const viewportHeight = globalThis.innerHeight + globalThis.scrollY; + return ( + focusedFieldRectsTop && + focusedFieldRectsTop > 0 && + focusedFieldRectsTop < viewportHeight && + focusedFieldRectsBottom < viewportHeight + ); + } + + /** + * Clears the timeout that triggers a debounced focus of the inline menu list. + */ + private clearFocusInlineMenuListTimeout() { + if (this.focusInlineMenuListTimeout) { + globalThis.clearTimeout(this.focusInlineMenuListTimeout); + } + } + + /** + * Clears the timeout that triggers the closing of the inline menu on a focus redirection. + */ + private clearCloseInlineMenuOnRedirectTimeout() { + if (this.closeInlineMenuOnRedirectTimeout) { + globalThis.clearTimeout(this.closeInlineMenuOnRedirectTimeout); + } + } + /** * Destroys the autofill overlay content service. This method will * disconnect the mutation observers and remove all event listeners. */ destroy() { - this.documentElementMutationObserver?.disconnect(); - this.clearUserInteractionEventTimeout(); + this.clearFocusInlineMenuListTimeout(); + this.clearCloseInlineMenuOnRedirectTimeout(); this.formFieldElements.forEach((formFieldElement) => { this.removeCachedFormFieldEventListeners(formFieldElement); formFieldElement.removeEventListener(EVENTS.BLUR, this.handleFormFieldBlurEvent); formFieldElement.removeEventListener(EVENTS.KEYUP, this.handleFormFieldKeyupEvent); this.formFieldElements.delete(formFieldElement); }); + globalThis.removeEventListener(EVENTS.MESSAGE, this.handleWindowMessageEvent); globalThis.document.removeEventListener( EVENTS.VISIBILITYCHANGE, this.handleVisibilityChangeEvent, ); globalThis.removeEventListener(EVENTS.FOCUSOUT, this.handleFormFieldBlurEvent); - this.removeAutofillOverlay(); this.removeOverlayRepositionEventListeners(); + this.removeRebuildSubFrameOffsetsListeners(); + this.removeSubFrameFocusOutListeners(); } } - -export default AutofillOverlayContentService; diff --git a/apps/browser/src/autofill/services/autofill.service.spec.ts b/apps/browser/src/autofill/services/autofill.service.spec.ts index dc9f3fcdbd4..ce7f4d41d26 100644 --- a/apps/browser/src/autofill/services/autofill.service.spec.ts +++ b/apps/browser/src/autofill/services/autofill.service.spec.ts @@ -5,7 +5,7 @@ import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { UserVerificationService } from "@bitwarden/common/auth/services/user-verification/user-verification.service"; import { AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants"; -import { AutofillSettingsService } from "@bitwarden/common/autofill/services/autofill-settings.service"; +import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; import { DefaultDomainSettingsService, DomainSettingsService, @@ -14,6 +14,7 @@ import { InlineMenuVisibilitySetting } from "@bitwarden/common/autofill/types"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { EventType } from "@bitwarden/common/enums"; import { UriMatchStrategy } from "@bitwarden/common/models/domain/domain-service"; +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 { MessageListener } from "@bitwarden/common/platform/messaging"; @@ -40,7 +41,7 @@ import { TotpService } from "@bitwarden/common/vault/services/totp.service"; import { BrowserApi } from "../../platform/browser/browser-api"; import { BrowserScriptInjectorService } from "../../platform/services/browser-script-injector.service"; import { AutofillMessageCommand, AutofillMessageSender } from "../enums/autofill-message.enums"; -import { AutofillPort } from "../enums/autofill-port.enums"; +import { AutofillPort } from "../enums/autofill-port.enum"; import AutofillField from "../models/autofill-field"; import AutofillPageDetails from "../models/autofill-page-details"; import AutofillScript from "../models/autofill-script"; @@ -72,7 +73,7 @@ describe("AutofillService", () => { let autofillService: AutofillService; const cipherService = mock(); let inlineMenuVisibilityMock$!: BehaviorSubject; - let autofillSettingsService: MockProxy; + let autofillSettingsService: MockProxy; const mockUserId = Utils.newGuid() as UserId; const accountService: FakeAccountService = mockAccountServiceWith(mockUserId); const fakeStateProvider: FakeStateProvider = new FakeStateProvider(accountService); @@ -86,16 +87,18 @@ describe("AutofillService", () => { const platformUtilsService = mock(); let activeAccountStatusMock$: BehaviorSubject; let authService: MockProxy; + let configService: MockProxy; let messageListener: MockProxy; beforeEach(() => { scriptInjectorService = new BrowserScriptInjectorService(platformUtilsService, logService); inlineMenuVisibilityMock$ = new BehaviorSubject(AutofillOverlayVisibility.OnFieldFocus); - autofillSettingsService = mock(); - (autofillSettingsService as any).inlineMenuVisibility$ = inlineMenuVisibilityMock$; + autofillSettingsService = mock(); + autofillSettingsService.inlineMenuVisibility$ = inlineMenuVisibilityMock$; activeAccountStatusMock$ = new BehaviorSubject(AuthenticationStatus.Unlocked); authService = mock(); authService.activeAccountStatus$ = activeAccountStatusMock$; + configService = mock(); messageListener = mock(); autofillService = new AutofillService( cipherService, @@ -109,6 +112,7 @@ describe("AutofillService", () => { scriptInjectorService, accountService, authService, + configService, messageListener, ); domainSettingsService = new DefaultDomainSettingsService(fakeStateProvider); @@ -213,7 +217,7 @@ describe("AutofillService", () => { .spyOn(BrowserApi, "getAllFrameDetails") .mockResolvedValue([mock({ frameId: 0 })]); jest - .spyOn(autofillService, "getOverlayVisibility") + .spyOn(autofillService, "getInlineMenuVisibility") .mockResolvedValue(AutofillOverlayVisibility.OnFieldFocus); jest.spyOn(autofillService, "getAutofillOnPageLoad").mockResolvedValue(true); }); @@ -275,13 +279,13 @@ describe("AutofillService", () => { expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith( tab1, - "updateAutofillOverlayVisibility", - { autofillOverlayVisibility: AutofillOverlayVisibility.OnButtonClick }, + "updateAutofillInlineMenuVisibility", + { inlineMenuVisibility: AutofillOverlayVisibility.OnButtonClick }, ); expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith( tab2, - "updateAutofillOverlayVisibility", - { autofillOverlayVisibility: AutofillOverlayVisibility.OnButtonClick }, + "updateAutofillInlineMenuVisibility", + { inlineMenuVisibility: AutofillOverlayVisibility.OnButtonClick }, ); }); @@ -292,13 +296,13 @@ describe("AutofillService", () => { expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith( tab1, - "updateAutofillOverlayVisibility", - { autofillOverlayVisibility: AutofillOverlayVisibility.OnFieldFocus }, + "updateAutofillInlineMenuVisibility", + { inlineMenuVisibility: AutofillOverlayVisibility.OnFieldFocus }, ); expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith( tab2, - "updateAutofillOverlayVisibility", - { autofillOverlayVisibility: AutofillOverlayVisibility.OnFieldFocus }, + "updateAutofillInlineMenuVisibility", + { inlineMenuVisibility: AutofillOverlayVisibility.OnFieldFocus }, ); }); }); @@ -351,11 +355,12 @@ describe("AutofillService", () => { let sender: chrome.runtime.MessageSender; beforeEach(() => { + configService.getFeatureFlag.mockResolvedValue(true); tabMock = createChromeTabMock(); sender = { tab: tabMock, frameId: 1 }; jest.spyOn(BrowserApi, "executeScriptInTab").mockImplementation(); jest - .spyOn(autofillService, "getOverlayVisibility") + .spyOn(autofillService, "getInlineMenuVisibility") .mockResolvedValue(AutofillOverlayVisibility.OnFieldFocus); jest.spyOn(autofillService, "getAutofillOnPageLoad").mockResolvedValue(true); }); @@ -413,7 +418,7 @@ describe("AutofillService", () => { it("will inject the bootstrap-autofill script if the user does not have the autofill overlay enabled", async () => { jest - .spyOn(autofillService, "getOverlayVisibility") + .spyOn(autofillService, "getInlineMenuVisibility") .mockResolvedValue(AutofillOverlayVisibility.Off); await autofillService.injectAutofillScripts(sender.tab, sender.frameId); diff --git a/apps/browser/src/autofill/services/autofill.service.ts b/apps/browser/src/autofill/services/autofill.service.ts index 4c37cd1f07f..81a47b2f614 100644 --- a/apps/browser/src/autofill/services/autofill.service.ts +++ b/apps/browser/src/autofill/services/autofill.service.ts @@ -12,10 +12,12 @@ import { DomainSettingsService } from "@bitwarden/common/autofill/services/domai import { InlineMenuVisibilitySetting } from "@bitwarden/common/autofill/types"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { EventType } from "@bitwarden/common/enums"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { UriMatchStrategySetting, UriMatchStrategy, } from "@bitwarden/common/models/domain/domain-service"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { MessageListener } from "@bitwarden/common/platform/messaging"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; @@ -29,7 +31,7 @@ import { BrowserApi } from "../../platform/browser/browser-api"; import { ScriptInjectorService } from "../../platform/services/abstractions/script-injector.service"; import { openVaultItemPasswordRepromptPopout } from "../../vault/popup/utils/vault-popout-window"; import { AutofillMessageCommand, AutofillMessageSender } from "../enums/autofill-message.enums"; -import { AutofillPort } from "../enums/autofill-port.enums"; +import { AutofillPort } from "../enums/autofill-port.enum"; import AutofillField from "../models/autofill-field"; import AutofillPageDetails from "../models/autofill-page-details"; import AutofillScript from "../models/autofill-script"; @@ -67,6 +69,7 @@ export default class AutofillService implements AutofillServiceInterface { private scriptInjectorService: ScriptInjectorService, private accountService: AccountService, private authService: AuthService, + private configService: ConfigService, private messageListener: MessageListener, ) {} @@ -160,16 +163,23 @@ export default class AutofillService implements AutofillServiceInterface { const activeAccount = await firstValueFrom(this.accountService.activeAccount$); const authStatus = await firstValueFrom(this.authService.activeAccountStatus$); const accountIsUnlocked = authStatus === AuthenticationStatus.Unlocked; - let overlayVisibility: InlineMenuVisibilitySetting = AutofillOverlayVisibility.Off; + let inlineMenuVisibility: InlineMenuVisibilitySetting = AutofillOverlayVisibility.Off; let autoFillOnPageLoadIsEnabled = false; if (activeAccount) { - overlayVisibility = await this.getOverlayVisibility(); + inlineMenuVisibility = await this.getInlineMenuVisibility(); } - const mainAutofillScript = overlayVisibility - ? "bootstrap-autofill-overlay.js" - : "bootstrap-autofill.js"; + let mainAutofillScript = "bootstrap-autofill.js"; + + if (inlineMenuVisibility) { + const inlineMenuPositioningImprovements = await this.configService.getFeatureFlag( + FeatureFlag.InlineMenuPositioningImprovements, + ); + mainAutofillScript = inlineMenuPositioningImprovements + ? "bootstrap-autofill-overlay.js" + : "bootstrap-legacy-autofill-overlay.js"; + } const injectedScripts = [mainAutofillScript]; @@ -274,7 +284,7 @@ export default class AutofillService implements AutofillServiceInterface { /** * Gets the overlay's visibility setting from the autofill settings service. */ - async getOverlayVisibility(): Promise { + async getInlineMenuVisibility(): Promise { return await firstValueFrom(this.autofillSettingsService.inlineMenuVisibility$); } @@ -2162,8 +2172,8 @@ export default class AutofillService implements AutofillServiceInterface { if (!inlineMenuPreviouslyDisabled && !inlineMenuCurrentlyDisabled) { const tabs = await BrowserApi.tabsQuery({}); tabs.forEach((tab) => - BrowserApi.tabSendMessageData(tab, "updateAutofillOverlayVisibility", { - autofillOverlayVisibility: currentSetting, + BrowserApi.tabSendMessageData(tab, "updateAutofillInlineMenuVisibility", { + inlineMenuVisibility: currentSetting, }), ); return; diff --git a/apps/browser/src/autofill/services/collect-autofill-content.service.spec.ts b/apps/browser/src/autofill/services/collect-autofill-content.service.spec.ts index 9bb0e717a26..f67c0e88aa0 100644 --- a/apps/browser/src/autofill/services/collect-autofill-content.service.spec.ts +++ b/apps/browser/src/autofill/services/collect-autofill-content.service.spec.ts @@ -11,7 +11,8 @@ import { FormElementWithAttribute, } from "../types"; -import AutofillOverlayContentService from "./autofill-overlay-content.service"; +import { InlineMenuFieldQualificationService } from "./abstractions/inline-menu-field-qualifications.service"; +import { AutofillOverlayContentService } from "./autofill-overlay-content.service"; import CollectAutofillContentService from "./collect-autofill-content.service"; import DomElementVisibilityService from "./dom-element-visibility.service"; @@ -28,7 +29,10 @@ const waitForIdleCallback = () => new Promise((resolve) => globalThis.requestIdl describe("CollectAutofillContentService", () => { const domElementVisibilityService = new DomElementVisibilityService(); - const autofillOverlayContentService = new AutofillOverlayContentService(); + const inlineMenuFieldQualificationService = mock(); + const autofillOverlayContentService = new AutofillOverlayContentService( + inlineMenuFieldQualificationService, + ); let collectAutofillContentService: CollectAutofillContentService; const mockIntersectionObserver = mock(); const mockQuerySelectorAll = mockQuerySelectorAllDefinedCall(); @@ -250,7 +254,7 @@ describe("CollectAutofillContentService", () => { .mockResolvedValue(true); const setupAutofillOverlayListenerOnFieldSpy = jest.spyOn( collectAutofillContentService["autofillOverlayContentService"], - "setupAutofillOverlayListenerOnField", + "setupInlineMenu", ); await collectAutofillContentService.getPageDetails(); @@ -2564,7 +2568,7 @@ describe("CollectAutofillContentService", () => { ); setupAutofillOverlayListenerOnFieldSpy = jest.spyOn( collectAutofillContentService["autofillOverlayContentService"], - "setupAutofillOverlayListenerOnField", + "setupInlineMenu", ); }); @@ -2585,9 +2589,11 @@ describe("CollectAutofillContentService", () => { it("skips setting up the overlay listeners on a field that is not viewable", async () => { const formFieldElement = document.createElement("input") as ElementWithOpId; + const autofillField = mock(); const entries = [ { target: formFieldElement, isIntersecting: true }, ] as unknown as IntersectionObserverEntry[]; + collectAutofillContentService["autofillFieldElements"].set(formFieldElement, autofillField); isFormFieldViewableSpy.mockReturnValueOnce(false); await collectAutofillContentService["handleFormElementIntersection"](entries); @@ -2596,7 +2602,21 @@ describe("CollectAutofillContentService", () => { expect(setupAutofillOverlayListenerOnFieldSpy).not.toHaveBeenCalled(); }); - it("sets up the overlay listeners on a viewable field", async () => { + it("skips setting up the inline menu listeners if the observed form field is not present in the cache", async () => { + const formFieldElement = document.createElement("input") as ElementWithOpId; + const entries = [ + { target: formFieldElement, isIntersecting: true }, + ] as unknown as IntersectionObserverEntry[]; + isFormFieldViewableSpy.mockReturnValueOnce(true); + collectAutofillContentService["intersectionObserver"] = mockIntersectionObserver; + + await collectAutofillContentService["handleFormElementIntersection"](entries); + + expect(isFormFieldViewableSpy).not.toHaveBeenCalled(); + expect(setupAutofillOverlayListenerOnFieldSpy).not.toHaveBeenCalled(); + }); + + it("sets up the inline menu listeners on a viewable field", async () => { const formFieldElement = document.createElement("input") as ElementWithOpId; const autofillField = mock(); const entries = [ @@ -2616,4 +2636,17 @@ describe("CollectAutofillContentService", () => { ); }); }); + + describe("destroy", () => { + it("clears the updateAfterMutationIdleCallback", () => { + jest.spyOn(window, "clearTimeout"); + collectAutofillContentService["updateAfterMutationIdleCallback"] = setTimeout(jest.fn, 100); + + collectAutofillContentService.destroy(); + + expect(clearTimeout).toHaveBeenCalledWith( + collectAutofillContentService["updateAfterMutationIdleCallback"], + ); + }); + }); }); diff --git a/apps/browser/src/autofill/services/collect-autofill-content.service.ts b/apps/browser/src/autofill/services/collect-autofill-content.service.ts index 75c564e868e..b5541ba5eb6 100644 --- a/apps/browser/src/autofill/services/collect-autofill-content.service.ts +++ b/apps/browser/src/autofill/services/collect-autofill-content.service.ts @@ -1,12 +1,7 @@ import AutofillField from "../models/autofill-field"; import AutofillForm from "../models/autofill-form"; import AutofillPageDetails from "../models/autofill-page-details"; -import { - ElementWithOpId, - FillableFormFieldElement, - FormElementWithAttribute, - FormFieldElement, -} from "../types"; +import { ElementWithOpId, FillableFormFieldElement, FormFieldElement } from "../types"; import { elementIsDescriptionDetailsElement, elementIsDescriptionTermElement, @@ -21,6 +16,8 @@ import { nodeIsFormElement, nodeIsInputElement, // sendExtensionMessage, + getAttributeBoolean, + getPropertyOrAttribute, requestIdleCallbackPolyfill, cancelIdleCallbackPolyfill, } from "../utils"; @@ -37,6 +34,8 @@ import { DomElementVisibilityService } from "./abstractions/dom-element-visibili class CollectAutofillContentService implements CollectAutofillContentServiceInterface { private readonly domElementVisibilityService: DomElementVisibilityService; private readonly autofillOverlayContentService: AutofillOverlayContentService; + private readonly getAttributeBoolean = getAttributeBoolean; + private readonly getPropertyOrAttribute = getPropertyOrAttribute; private noFieldsFound = false; private domRecentlyMutated = true; private autofillFormElements: AutofillFormElements = new Map(); @@ -286,7 +285,7 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte autofillField.viewable = await this.domElementVisibilityService.isFormFieldViewable(element); if (!previouslyViewable && autofillField.viewable) { - this.setupInlineMenuListenerOnField(element, autofillField); + this.setupInlineMenu(element, autofillField); } }); } @@ -537,26 +536,6 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte ); } - /** - * Returns a boolean representing the attribute value of an element. - * @param {ElementWithOpId} element - * @param {string} attributeName - * @param {boolean} checkString - * @returns {boolean} - * @private - */ - private getAttributeBoolean( - element: ElementWithOpId, - attributeName: string, - checkString = false, - ): boolean { - if (checkString) { - return this.getPropertyOrAttribute(element, attributeName) === "true"; - } - - return Boolean(this.getPropertyOrAttribute(element, attributeName)); - } - /** * Returns the attribute of an element as a lowercase value. * @param {ElementWithOpId} element @@ -868,21 +847,6 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte return this.recursivelyGetTextFromPreviousSiblings(siblingElement); } - /** - * Get the value of a property or attribute from a FormFieldElement. - * @param {HTMLElement} element - * @param {string} attributeName - * @returns {string | null} - * @private - */ - private getPropertyOrAttribute(element: HTMLElement, attributeName: string): string | null { - if (attributeName in element) { - return (element as FormElementWithAttribute)[attributeName]; - } - - return element.getAttribute(attributeName); - } - /** * Gets the value of the element. If the element is a checkbox, returns a checkmark if the * checkbox is checked, or an empty string if it is not checked. If the element is a hidden @@ -1411,20 +1375,20 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte continue; } + const cachedAutofillFieldElement = this.autofillFieldElements.get(formFieldElement); + if (!cachedAutofillFieldElement) { + this.intersectionObserver.unobserve(entry.target); + continue; + } + const isViewable = await this.domElementVisibilityService.isFormFieldViewable(formFieldElement); if (!isViewable) { continue; } - const cachedAutofillFieldElement = this.autofillFieldElements.get(formFieldElement); - if (!cachedAutofillFieldElement) { - continue; - } - cachedAutofillFieldElement.viewable = true; - - this.setupInlineMenuListenerOnField(formFieldElement, cachedAutofillFieldElement); + this.setupInlineMenu(formFieldElement, cachedAutofillFieldElement); this.intersectionObserver?.unobserve(entry.target); } @@ -1441,7 +1405,7 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte } this.autofillFieldElements.forEach((autofillField, formFieldElement) => { - this.setupInlineMenuListenerOnField(formFieldElement, autofillField, pageDetails); + this.setupInlineMenu(formFieldElement, autofillField, pageDetails); }); } @@ -1452,7 +1416,7 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte * @param autofillField - The metadata for the form field * @param pageDetails - The page details to use for the inline menu listeners */ - private setupInlineMenuListenerOnField( + private setupInlineMenu( formFieldElement: ElementWithOpId, autofillField: AutofillField, pageDetails?: AutofillPageDetails, @@ -1468,7 +1432,7 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte this.getFormattedAutofillFieldsData(), ); - void this.autofillOverlayContentService.setupAutofillOverlayListenerOnField( + void this.autofillOverlayContentService.setupInlineMenu( formFieldElement, autofillField, autofillPageDetails, diff --git a/apps/browser/src/autofill/services/dom-element-visibility.service.ts b/apps/browser/src/autofill/services/dom-element-visibility.service.ts index 127ce84d919..67986eb00f2 100644 --- a/apps/browser/src/autofill/services/dom-element-visibility.service.ts +++ b/apps/browser/src/autofill/services/dom-element-visibility.service.ts @@ -1,3 +1,4 @@ +import { AutofillInlineMenuContentService } from "../overlay/inline-menu/abstractions/autofill-inline-menu-content.service"; import { FillableFormFieldElement, FormFieldElement } from "../types"; import { DomElementVisibilityService as domElementVisibilityServiceInterface } from "./abstractions/dom-element-visibility.service"; @@ -5,6 +6,8 @@ import { DomElementVisibilityService as domElementVisibilityServiceInterface } f class DomElementVisibilityService implements domElementVisibilityServiceInterface { private cachedComputedStyle: CSSStyleDeclaration | null = null; + constructor(private inlineMenuElements?: AutofillInlineMenuContentService) {} + /** * Checks if a form field is viewable. This is done by checking if the element is within the * viewport bounds, not hidden by CSS, and not hidden behind another element. @@ -187,6 +190,10 @@ class DomElementVisibilityService implements domElementVisibilityServiceInterfac return true; } + if (this.inlineMenuElements?.isElementInlineMenu(elementAtCenterPoint as HTMLElement)) { + return true; + } + const targetElementLabelsSet = new Set((targetElement as FillableFormFieldElement).labels); if (targetElementLabelsSet.has(elementAtCenterPoint as HTMLLabelElement)) { return true; diff --git a/apps/browser/src/autofill/services/inline-menu-field-qualification.service.ts b/apps/browser/src/autofill/services/inline-menu-field-qualification.service.ts index 7bc027b392c..a6253dffac2 100644 --- a/apps/browser/src/autofill/services/inline-menu-field-qualification.service.ts +++ b/apps/browser/src/autofill/services/inline-menu-field-qualification.service.ts @@ -2,11 +2,11 @@ import AutofillField from "../models/autofill-field"; import AutofillPageDetails from "../models/autofill-page-details"; import { sendExtensionMessage } from "../utils"; -import { InlineMenuFieldQualificationsService as InlineMenuFieldQualificationsServiceInterface } from "./abstractions/inline-menu-field-qualifications.service"; +import { InlineMenuFieldQualificationService as InlineMenuFieldQualificationServiceInterface } from "./abstractions/inline-menu-field-qualifications.service"; import { AutoFillConstants } from "./autofill-constants"; export class InlineMenuFieldQualificationService - implements InlineMenuFieldQualificationsServiceInterface + implements InlineMenuFieldQualificationServiceInterface { private searchFieldNamesSet = new Set(AutoFillConstants.SearchFieldNames); private excludedAutofillLoginTypesSet = new Set(AutoFillConstants.ExcludedAutofillLoginTypes); 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 6ee5171e58c..ff0e82d664d 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 @@ -1,10 +1,13 @@ +import { mock } from "jest-mock-extended"; + import { EVENTS } from "@bitwarden/common/autofill/constants"; import AutofillScript, { FillScript, FillScriptActions } from "../models/autofill-script"; import { mockQuerySelectorAllDefinedCall } from "../spec/testing-utils"; import { FillableFormFieldElement, FormElementWithAttribute, FormFieldElement } from "../types"; -import AutofillOverlayContentService from "./autofill-overlay-content.service"; +import { InlineMenuFieldQualificationService } from "./abstractions/inline-menu-field-qualifications.service"; +import { AutofillOverlayContentService } from "./autofill-overlay-content.service"; import CollectAutofillContentService from "./collect-autofill-content.service"; import DomElementVisibilityService from "./dom-element-visibility.service"; import InsertAutofillContentService from "./insert-autofill-content.service"; @@ -64,8 +67,11 @@ function setMockWindowLocation({ } describe("InsertAutofillContentService", () => { + const inlineMenuFieldQualificationService = mock(); const domElementVisibilityService = new DomElementVisibilityService(); - const autofillOverlayContentService = new AutofillOverlayContentService(); + const autofillOverlayContentService = new AutofillOverlayContentService( + inlineMenuFieldQualificationService, + ); const collectAutofillContentService = new CollectAutofillContentService( domElementVisibilityService, autofillOverlayContentService, diff --git a/apps/browser/src/autofill/spec/autofill-mocks.ts b/apps/browser/src/autofill/spec/autofill-mocks.ts index 021b7719b2b..2d4ffd7f217 100644 --- a/apps/browser/src/autofill/spec/autofill-mocks.ts +++ b/apps/browser/src/autofill/spec/autofill-mocks.ts @@ -7,16 +7,16 @@ import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; -import { OverlayCipherData } from "../background/abstractions/overlay.background"; +import { InlineMenuCipherData } from "../background/abstractions/overlay.background"; import AutofillField from "../models/autofill-field"; import AutofillForm from "../models/autofill-form"; import AutofillPageDetails from "../models/autofill-page-details"; import AutofillScript, { FillScript } from "../models/autofill-script"; -import { InitAutofillOverlayButtonMessage } from "../overlay/abstractions/autofill-overlay-button"; -import { InitAutofillOverlayListMessage } from "../overlay/abstractions/autofill-overlay-list"; +import { InitAutofillInlineMenuButtonMessage } from "../overlay/inline-menu/abstractions/autofill-inline-menu-button"; +import { InitAutofillInlineMenuListMessage } from "../overlay/inline-menu/abstractions/autofill-inline-menu-list"; import { GenerateFillScriptOptions, PageDetail } from "../services/abstractions/autofill.service"; -function createAutofillFormMock(customFields = {}): AutofillForm { +export function createAutofillFormMock(customFields = {}): AutofillForm { return { opid: "default-form-opid", htmlID: "default-htmlID", @@ -27,7 +27,7 @@ function createAutofillFormMock(customFields = {}): AutofillForm { }; } -function createAutofillFieldMock(customFields = {}): AutofillField { +export function createAutofillFieldMock(customFields = {}): AutofillField { return { opid: "default-input-field-opid", elementNumber: 0, @@ -57,7 +57,7 @@ function createAutofillFieldMock(customFields = {}): AutofillField { }; } -function createPageDetailMock(customFields = {}): PageDetail { +export function createPageDetailMock(customFields = {}): PageDetail { return { frameId: 0, tab: createChromeTabMock(), @@ -66,7 +66,7 @@ function createPageDetailMock(customFields = {}): PageDetail { }; } -function createAutofillPageDetailsMock(customFields = {}): AutofillPageDetails { +export function createAutofillPageDetailsMock(customFields = {}): AutofillPageDetails { return { title: "title", url: "url", @@ -86,7 +86,7 @@ function createAutofillPageDetailsMock(customFields = {}): AutofillPageDetails { }; } -function createChromeTabMock(customFields = {}): chrome.tabs.Tab { +export function createChromeTabMock(customFields = {}): chrome.tabs.Tab { return { id: 1, index: 1, @@ -104,7 +104,7 @@ function createChromeTabMock(customFields = {}): chrome.tabs.Tab { }; } -function createGenerateFillScriptOptionsMock(customFields = {}): GenerateFillScriptOptions { +export function createGenerateFillScriptOptionsMock(customFields = {}): GenerateFillScriptOptions { return { skipUsernameOnlyFill: false, onlyEmptyFields: false, @@ -118,7 +118,7 @@ function createGenerateFillScriptOptionsMock(customFields = {}): GenerateFillScr }; } -function createAutofillScriptMock( +export function createAutofillScriptMock( customFields = {}, scriptTypes?: Record, ): AutofillScript { @@ -159,24 +159,28 @@ const overlayPagesTranslations = { unlockYourAccount: "unlockYourAccount", unlockAccount: "unlockAccount", fillCredentialsFor: "fillCredentialsFor", - partialUsername: "partialUsername", + username: "username", view: "view", noItemsToShow: "noItemsToShow", newItem: "newItem", addNewVaultItem: "addNewVaultItem", }; -function createInitAutofillOverlayButtonMessageMock( +export function createInitAutofillInlineMenuButtonMessageMock( customFields = {}, -): InitAutofillOverlayButtonMessage { +): InitAutofillInlineMenuButtonMessage { return { - command: "initAutofillOverlayButton", + command: "initAutofillInlineMenuButton", translations: overlayPagesTranslations, styleSheetUrl: "https://jest-testing-website.com", authStatus: AuthenticationStatus.Unlocked, + portKey: "portKey", ...customFields, }; } -function createAutofillOverlayCipherDataMock(index: number, customFields = {}): OverlayCipherData { +export function createAutofillOverlayCipherDataMock( + index: number, + customFields = {}, +): InlineMenuCipherData { return { id: String(index), name: `website login ${index}`, @@ -194,15 +198,16 @@ function createAutofillOverlayCipherDataMock(index: number, customFields = {}): }; } -function createInitAutofillOverlayListMessageMock( +export function createInitAutofillInlineMenuListMessageMock( customFields = {}, -): InitAutofillOverlayListMessage { +): InitAutofillInlineMenuListMessage { return { - command: "initAutofillOverlayList", + command: "initAutofillInlineMenuList", translations: overlayPagesTranslations, styleSheetUrl: "https://jest-testing-website.com", theme: ThemeType.Light, authStatus: AuthenticationStatus.Unlocked, + portKey: "portKey", ciphers: [ createAutofillOverlayCipherDataMock(1, { icon: { @@ -237,7 +242,7 @@ function createInitAutofillOverlayListMessageMock( }; } -function createFocusedFieldDataMock(customFields = {}) { +export function createFocusedFieldDataMock(customFields = {}) { return { focusedFieldRects: { top: 1, @@ -250,11 +255,12 @@ function createFocusedFieldDataMock(customFields = {}) { paddingLeft: "6px", }, tabId: 1, + frameId: 2, ...customFields, }; } -function createPortSpyMock(name: string) { +export function createPortSpyMock(name: string) { return mock({ name, onMessage: { @@ -273,16 +279,17 @@ function createPortSpyMock(name: string) { }); } -export { - createAutofillFormMock, - createAutofillFieldMock, - createPageDetailMock, - createAutofillPageDetailsMock, - createChromeTabMock, - createGenerateFillScriptOptionsMock, - createAutofillScriptMock, - createInitAutofillOverlayButtonMessageMock, - createInitAutofillOverlayListMessageMock, - createFocusedFieldDataMock, - createPortSpyMock, -}; +export function createMutationRecordMock(customFields = {}): MutationRecord { + return { + addedNodes: mock(), + attributeName: "default-attributeName", + attributeNamespace: "default-attributeNamespace", + nextSibling: null, + oldValue: "default-oldValue", + previousSibling: null, + removedNodes: mock(), + target: null, + type: "attributes", + ...customFields, + }; +} diff --git a/apps/browser/src/autofill/spec/testing-utils.ts b/apps/browser/src/autofill/spec/testing-utils.ts index ba7a5844987..1cef5186028 100644 --- a/apps/browser/src/autofill/spec/testing-utils.ts +++ b/apps/browser/src/autofill/spec/testing-utils.ts @@ -48,6 +48,22 @@ export function sendPortMessage(port: chrome.runtime.Port, message: any) { }); } +export function triggerPortOnConnectEvent(port: chrome.runtime.Port) { + (chrome.runtime.onConnect.addListener as unknown as jest.SpyInstance).mock.calls.forEach( + (call) => { + const callback = call[0]; + callback(port); + }, + ); +} + +export function triggerPortOnMessageEvent(port: chrome.runtime.Port, message: any) { + (port.onMessage.addListener as unknown as jest.SpyInstance).mock.calls.forEach((call) => { + const callback = call[0]; + callback(message, port); + }); +} + export function triggerPortOnDisconnectEvent(port: chrome.runtime.Port) { (port.onDisconnect.addListener as unknown as jest.SpyInstance).mock.calls.forEach((call) => { const callback = call[0]; @@ -105,6 +121,17 @@ export function triggerOnAlarmEvent(alarm: chrome.alarms.Alarm) { }); } +export function triggerWebNavigationOnCommittedEvent( + details: chrome.webNavigation.WebNavigationFramedCallbackDetails, +) { + (chrome.webNavigation.onCommitted.addListener as unknown as jest.SpyInstance).mock.calls.forEach( + (call) => { + const callback = call[0]; + callback(details); + }, + ); +} + export function mockQuerySelectorAllDefinedCall() { const originalDocumentQuerySelectorAll = document.querySelectorAll; document.querySelectorAll = function (selector: string) { diff --git a/apps/browser/src/autofill/utils/autofill-overlay.enum.ts b/apps/browser/src/autofill/utils/autofill-overlay.enum.ts deleted file mode 100644 index 486d68f7540..00000000000 --- a/apps/browser/src/autofill/utils/autofill-overlay.enum.ts +++ /dev/null @@ -1,17 +0,0 @@ -const AutofillOverlayElement = { - Button: "autofill-overlay-button", - List: "autofill-overlay-list", -} as const; - -const AutofillOverlayPort = { - Button: "autofill-overlay-button-port", - List: "autofill-overlay-list-port", -} as const; - -const RedirectFocusDirection = { - Current: "current", - Previous: "previous", - Next: "next", -} as const; - -export { AutofillOverlayElement, AutofillOverlayPort, RedirectFocusDirection }; diff --git a/apps/browser/src/autofill/utils/index.spec.ts b/apps/browser/src/autofill/utils/index.spec.ts index dcb5aa64696..116df044b37 100644 --- a/apps/browser/src/autofill/utils/index.spec.ts +++ b/apps/browser/src/autofill/utils/index.spec.ts @@ -1,4 +1,4 @@ -import { AutofillPort } from "../enums/autofill-port.enums"; +import { AutofillPort } from "../enums/autofill-port.enum"; import { triggerPortOnDisconnectEvent } from "../spec/testing-utils"; import { logoIcon, logoLockedIcon } from "./svg-icons"; @@ -38,9 +38,7 @@ describe("generateRandomCustomElementName", () => { describe("sendExtensionMessage", () => { it("sends a message to the extension", async () => { - const extensionMessagePromise = sendExtensionMessage("updateAutofillOverlayHidden", { - display: "none", - }); + const extensionMessagePromise = sendExtensionMessage("some-extension-message"); // Jest doesn't give anyway to select the typed overload of "sendMessage", // a cast is needed to get the correct spy type. diff --git a/apps/browser/src/autofill/utils/index.ts b/apps/browser/src/autofill/utils/index.ts index 873012d1dbb..a040fa50122 100644 --- a/apps/browser/src/autofill/utils/index.ts +++ b/apps/browser/src/autofill/utils/index.ts @@ -1,5 +1,24 @@ -import { AutofillPort } from "../enums/autofill-port.enums"; -import { FillableFormFieldElement, FormFieldElement } from "../types"; +import { AutofillPort } from "../enums/autofill-port.enum"; +import { FillableFormFieldElement, FormElementWithAttribute, FormFieldElement } from "../types"; + +/** + * Generates a random string of characters. + * + * @param length - The length of the random string to generate. + */ +export function generateRandomChars(length: number): string { + const chars = "abcdefghijklmnopqrstuvwxyz"; + const randomChars = []; + const randomBytes = new Uint8Array(length); + globalThis.crypto.getRandomValues(randomBytes); + + for (let byteIndex = 0; byteIndex < randomBytes.length; byteIndex++) { + const byte = randomBytes[byteIndex]; + randomChars.push(chars[byte % chars.length]); + } + + return randomChars.join(""); +} /** * Polyfills the requestIdleCallback API with a setTimeout fallback. @@ -34,21 +53,7 @@ export function cancelIdleCallbackPolyfill(id: NodeJS.Timeout | number) { /** * Generates a random string of characters that formatted as a custom element name. */ -function generateRandomCustomElementName(): string { - const generateRandomChars = (length: number): string => { - const chars = "abcdefghijklmnopqrstuvwxyz"; - const randomChars = []; - const randomBytes = new Uint8Array(length); - globalThis.crypto.getRandomValues(randomBytes); - - for (let byteIndex = 0; byteIndex < randomBytes.length; byteIndex++) { - const byte = randomBytes[byteIndex]; - randomChars.push(chars[byte % chars.length]); - } - - return randomChars.join(""); - }; - +export function generateRandomCustomElementName(): string { const length = Math.floor(Math.random() * 5) + 8; // Between 8 and 12 characters const numHyphens = Math.min(Math.max(Math.floor(Math.random() * 4), 1), length - 1); // At least 1, maximum of 3 hyphens @@ -81,7 +86,7 @@ function generateRandomCustomElementName(): string { * @param svgString - The SVG string to build the DOM element from. * @param ariaHidden - Determines whether the SVG should be hidden from screen readers. */ -function buildSvgDomElement(svgString: string, ariaHidden = true): HTMLElement { +export function buildSvgDomElement(svgString: string, ariaHidden = true): HTMLElement { const domParser = new DOMParser(); const svgDom = domParser.parseFromString(svgString, "image/svg+xml"); const domElement = svgDom.documentElement; @@ -96,14 +101,14 @@ function buildSvgDomElement(svgString: string, ariaHidden = true): HTMLElement { * @param command - The command to send. * @param options - The options to send with the command. */ -async function sendExtensionMessage( +export async function sendExtensionMessage( command: string, options: Record = {}, ): Promise { return new Promise((resolve) => { chrome.runtime.sendMessage(Object.assign({ command }, options), (response) => { if (chrome.runtime.lastError) { - return; + // Do nothing } resolve(response); @@ -118,7 +123,7 @@ async function sendExtensionMessage( * @param styles - The styles to set on the element. * @param priority - Determines whether the styles should be set as important. */ -function setElementStyles( +export function setElementStyles( element: HTMLElement, styles: Partial, priority?: boolean, @@ -141,9 +146,9 @@ function setElementStyles( * and triggers an onDisconnect event if the extension context * is invalidated. * - * @param callback - Callback function to run when the extension disconnects + * @param callback - Callback export function to run when the extension disconnects */ -function setupExtensionDisconnectAction(callback: (port: chrome.runtime.Port) => void) { +export function setupExtensionDisconnectAction(callback: (port: chrome.runtime.Port) => void) { const port = chrome.runtime.connect({ name: AutofillPort.InjectedScript }); const onDisconnectCallback = (disconnectedPort: chrome.runtime.Port) => { callback(disconnectedPort); @@ -158,7 +163,7 @@ function setupExtensionDisconnectAction(callback: (port: chrome.runtime.Port) => * * @param windowContext - The global window context */ -function setupAutofillInitDisconnectAction(windowContext: Window) { +export function setupAutofillInitDisconnectAction(windowContext: Window) { if (!windowContext.bitwardenAutofillInit) { return; } @@ -176,10 +181,10 @@ function setupAutofillInitDisconnectAction(windowContext: Window) { * * @param formFieldElement - The form field element to check. */ -function elementIsFillableFormField( +export function elementIsFillableFormField( formFieldElement: FormFieldElement, ): formFieldElement is FillableFormFieldElement { - return formFieldElement?.tagName.toLowerCase() !== "span"; + return !elementIsSpanElement(formFieldElement); } /** @@ -188,8 +193,11 @@ function elementIsFillableFormField( * @param element - The element to check. * @param tagName - The tag name to check against. */ -function elementIsInstanceOf(element: Element, tagName: string): element is T { - return element?.tagName.toLowerCase() === tagName; +export function elementIsInstanceOf( + element: Element, + tagName: string, +): element is T { + return nodeIsElement(element) && element.tagName.toLowerCase() === tagName; } /** @@ -197,7 +205,7 @@ function elementIsInstanceOf(element: Element, tagName: strin * * @param element - The element to check. */ -function elementIsSpanElement(element: Element): element is HTMLSpanElement { +export function elementIsSpanElement(element: Element): element is HTMLSpanElement { return elementIsInstanceOf(element, "span"); } @@ -206,7 +214,7 @@ function elementIsSpanElement(element: Element): element is HTMLSpanElement { * * @param element - The element to check. */ -function elementIsInputElement(element: Element): element is HTMLInputElement { +export function elementIsInputElement(element: Element): element is HTMLInputElement { return elementIsInstanceOf(element, "input"); } @@ -215,7 +223,7 @@ function elementIsInputElement(element: Element): element is HTMLInputElement { * * @param element - The element to check. */ -function elementIsSelectElement(element: Element): element is HTMLSelectElement { +export function elementIsSelectElement(element: Element): element is HTMLSelectElement { return elementIsInstanceOf(element, "select"); } @@ -224,7 +232,7 @@ function elementIsSelectElement(element: Element): element is HTMLSelectElement * * @param element - The element to check. */ -function elementIsTextAreaElement(element: Element): element is HTMLTextAreaElement { +export function elementIsTextAreaElement(element: Element): element is HTMLTextAreaElement { return elementIsInstanceOf(element, "textarea"); } @@ -233,7 +241,7 @@ function elementIsTextAreaElement(element: Element): element is HTMLTextAreaElem * * @param element - The element to check. */ -function elementIsFormElement(element: Element): element is HTMLFormElement { +export function elementIsFormElement(element: Element): element is HTMLFormElement { return elementIsInstanceOf(element, "form"); } @@ -242,7 +250,7 @@ function elementIsFormElement(element: Element): element is HTMLFormElement { * * @param element - The element to check. */ -function elementIsLabelElement(element: Element): element is HTMLLabelElement { +export function elementIsLabelElement(element: Element): element is HTMLLabelElement { return elementIsInstanceOf(element, "label"); } @@ -251,7 +259,7 @@ function elementIsLabelElement(element: Element): element is HTMLLabelElement { * * @param element - The element to check. */ -function elementIsDescriptionDetailsElement(element: Element): element is HTMLElement { +export function elementIsDescriptionDetailsElement(element: Element): element is HTMLElement { return elementIsInstanceOf(element, "dd"); } @@ -260,7 +268,7 @@ function elementIsDescriptionDetailsElement(element: Element): element is HTMLEl * * @param element - The element to check. */ -function elementIsDescriptionTermElement(element: Element): element is HTMLElement { +export function elementIsDescriptionTermElement(element: Element): element is HTMLElement { return elementIsInstanceOf(element, "dt"); } @@ -269,12 +277,12 @@ function elementIsDescriptionTermElement(element: Element): element is HTMLEleme * * @param node - The node to check. */ -function nodeIsElement(node: Node): node is Element { +export function nodeIsElement(node: Node): node is Element { if (!node) { return false; } - return node.nodeType === Node.ELEMENT_NODE; + return node?.nodeType === Node.ELEMENT_NODE; } /** @@ -282,7 +290,7 @@ function nodeIsElement(node: Node): node is Element { * * @param node - The node to check. */ -function nodeIsInputElement(node: Node): node is HTMLInputElement { +export function nodeIsInputElement(node: Node): node is HTMLInputElement { return nodeIsElement(node) && elementIsInputElement(node); } @@ -291,28 +299,56 @@ function nodeIsInputElement(node: Node): node is HTMLInputElement { * * @param node - The node to check. */ -function nodeIsFormElement(node: Node): node is HTMLFormElement { +export function nodeIsFormElement(node: Node): node is HTMLFormElement { return nodeIsElement(node) && elementIsFormElement(node); } -export { - generateRandomCustomElementName, - buildSvgDomElement, - sendExtensionMessage, - setElementStyles, - setupExtensionDisconnectAction, - setupAutofillInitDisconnectAction, - elementIsFillableFormField, - elementIsInstanceOf, - elementIsSpanElement, - elementIsInputElement, - elementIsSelectElement, - elementIsTextAreaElement, - elementIsFormElement, - elementIsLabelElement, - elementIsDescriptionDetailsElement, - elementIsDescriptionTermElement, - nodeIsElement, - nodeIsInputElement, - nodeIsFormElement, -}; +/** + * Returns a boolean representing the attribute value of an element. + * + * @param element + * @param attributeName + * @param checkString + */ +export function getAttributeBoolean( + element: HTMLElement, + attributeName: string, + checkString = false, +): boolean { + if (checkString) { + return getPropertyOrAttribute(element, attributeName) === "true"; + } + + return Boolean(getPropertyOrAttribute(element, attributeName)); +} + +/** + * Get the value of a property or attribute from a FormFieldElement. + * + * @param element + * @param attributeName + */ +export function getPropertyOrAttribute(element: HTMLElement, attributeName: string): string | null { + if (attributeName in element) { + return (element as FormElementWithAttribute)[attributeName]; + } + + return element.getAttribute(attributeName); +} + +/** + * Throttles a callback function to run at most once every `limit` milliseconds. + * + * @param callback - The callback function to throttle. + * @param limit - The time in milliseconds to throttle the callback. + */ +export function throttle(callback: () => void, limit: number) { + let waitingDelay = false; + return function (...args: unknown[]) { + if (!waitingDelay) { + callback.apply(this, args); + waitingDelay = true; + globalThis.setTimeout(() => (waitingDelay = false), limit); + } + }; +} diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 35e674cfd1c..9aac8464ab4 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -72,6 +72,7 @@ import { import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { DefaultBillingAccountProfileStateService } from "@bitwarden/common/billing/services/account/billing-account-profile-state.service"; import { ClientType } from "@bitwarden/common/enums"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { AppIdService as AppIdServiceAbstraction } from "@bitwarden/common/platform/abstractions/app-id.service"; import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; @@ -197,14 +198,16 @@ import { VaultExportServiceAbstraction, } from "@bitwarden/vault-export-core"; +import { OverlayBackground as OverlayBackgroundInterface } from "../autofill/background/abstractions/overlay.background"; import ContextMenusBackground from "../autofill/background/context-menus.background"; import NotificationBackground from "../autofill/background/notification.background"; -import OverlayBackground from "../autofill/background/overlay.background"; +import { OverlayBackground } from "../autofill/background/overlay.background"; import TabsBackground from "../autofill/background/tabs.background"; import WebRequestBackground from "../autofill/background/web-request.background"; import { CipherContextMenuHandler } from "../autofill/browser/cipher-context-menu-handler"; import { ContextMenuClickedHandler } from "../autofill/browser/context-menu-clicked-handler"; import { MainContextMenuHandler } from "../autofill/browser/main-context-menu-handler"; +import LegacyOverlayBackground from "../autofill/deprecated/background/overlay.background.deprecated"; import { Fido2Background as Fido2BackgroundAbstraction } from "../autofill/fido2/background/abstractions/fido2.background"; import { Fido2Background } from "../autofill/fido2/background/fido2.background"; import { AutofillService as AutofillServiceAbstraction } from "../autofill/services/abstractions/autofill.service"; @@ -351,7 +354,7 @@ export default class MainBackground { private contextMenusBackground: ContextMenusBackground; private idleBackground: IdleBackground; private notificationBackground: NotificationBackground; - private overlayBackground: OverlayBackground; + private overlayBackground: OverlayBackgroundInterface; private filelessImporterBackground: FilelessImporterBackground; private runtimeBackground: RuntimeBackground; private tabsBackground: TabsBackground; @@ -901,6 +904,7 @@ export default class MainBackground { this.scriptInjectorService, this.accountService, this.authService, + this.configService, messageListener, ); this.auditService = new AuditService(this.cryptoFunctionService, this.apiService); @@ -1052,17 +1056,7 @@ export default class MainBackground { themeStateService, this.configService, ); - this.overlayBackground = new OverlayBackground( - this.cipherService, - this.autofillService, - this.authService, - this.environmentService, - this.domainSettingsService, - this.autofillSettingsService, - this.i18nService, - this.platformUtilsService, - themeStateService, - ); + this.filelessImporterBackground = new FilelessImporterBackground( this.configService, this.authService, @@ -1072,11 +1066,6 @@ export default class MainBackground { this.syncService, this.scriptInjectorService, ); - this.tabsBackground = new TabsBackground( - this, - this.notificationBackground, - this.overlayBackground, - ); const contextMenuClickedHandler = new ContextMenuClickedHandler( (options) => this.platformUtilsService.copyToClipboard(options.text), @@ -1156,6 +1145,47 @@ export default class MainBackground { } this.userAutoUnlockKeyService = new UserAutoUnlockKeyService(this.cryptoService); + + this.configService + .getFeatureFlag(FeatureFlag.InlineMenuPositioningImprovements) + .then(async (enabled) => { + if (!enabled) { + this.overlayBackground = new LegacyOverlayBackground( + this.cipherService, + this.autofillService, + this.authService, + this.environmentService, + this.domainSettingsService, + this.autofillSettingsService, + this.i18nService, + this.platformUtilsService, + themeStateService, + ); + } else { + this.overlayBackground = new OverlayBackground( + this.logService, + this.cipherService, + this.autofillService, + this.authService, + this.environmentService, + this.domainSettingsService, + this.autofillSettingsService, + this.i18nService, + this.platformUtilsService, + themeStateService, + ); + } + + this.tabsBackground = new TabsBackground( + this, + this.notificationBackground, + this.overlayBackground, + ); + + await this.overlayBackground.init(); + await this.tabsBackground.init(); + }) + .catch((error) => this.logService.error(`Error initializing OverlayBackground: ${error}`)); } async bootstrap() { @@ -1192,8 +1222,6 @@ export default class MainBackground { await this.notificationBackground.init(); this.filelessImporterBackground.init(); await this.commandsBackground.init(); - await this.overlayBackground.init(); - await this.tabsBackground.init(); this.contextMenusBackground?.init(); await this.idleBackground.init(); this.webRequestBackground?.startListening(); diff --git a/apps/browser/src/manifest.json b/apps/browser/src/manifest.json index b9ab9e0dd9d..1979d703641 100644 --- a/apps/browser/src/manifest.json +++ b/apps/browser/src/manifest.json @@ -67,7 +67,12 @@ "optional_permissions": ["nativeMessaging", "privacy"], "content_security_policy": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'", "sandbox": { - "pages": ["overlay/button.html", "overlay/list.html"], + "pages": [ + "overlay/menu-button.html", + "overlay/menu-list.html", + "overlay/button.html", + "overlay/list.html" + ], "content_security_policy": "sandbox allow-scripts; script-src 'self'" }, "commands": { @@ -107,6 +112,9 @@ "notification/bar.html", "images/icon38.png", "images/icon38_locked.png", + "overlay/menu-button.html", + "overlay/menu-list.html", + "overlay/menu.html", "overlay/button.html", "overlay/list.html", "popup/fonts/*" diff --git a/apps/browser/src/manifest.v3.json b/apps/browser/src/manifest.v3.json index b9eac49764d..c01117bfe1d 100644 --- a/apps/browser/src/manifest.v3.json +++ b/apps/browser/src/manifest.v3.json @@ -73,7 +73,12 @@ "sandbox": "sandbox allow-scripts; script-src 'self'" }, "sandbox": { - "pages": ["overlay/button.html", "overlay/list.html"] + "pages": [ + "overlay/menu-button.html", + "overlay/menu-list.html", + "overlay/button.html", + "overlay/list.html" + ] }, "commands": { "_execute_action": { @@ -113,6 +118,9 @@ "notification/bar.html", "images/icon38.png", "images/icon38_locked.png", + "overlay/menu-button.html", + "overlay/menu-list.html", + "overlay/menu.html", "overlay/button.html", "overlay/list.html", "popup/fonts/*" diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index c102f461a6e..01470f4d115 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -42,6 +42,7 @@ import { } from "@bitwarden/common/autofill/services/user-notification-settings.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { ClientType } from "@bitwarden/common/enums"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; @@ -314,6 +315,7 @@ const safeProviders: SafeProvider[] = [ ScriptInjectorService, AccountServiceAbstraction, AuthService, + ConfigService, MessageListener, ], }), diff --git a/apps/browser/webpack.config.js b/apps/browser/webpack.config.js index eb1244bc26d..e6ef80bcd9e 100644 --- a/apps/browser/webpack.config.js +++ b/apps/browser/webpack.config.js @@ -106,12 +106,27 @@ const plugins = [ chunks: ["notification/bar"], }), new HtmlWebpackPlugin({ - template: "./src/autofill/overlay/pages/button/button.html", + template: "./src/autofill/overlay/inline-menu/pages/button/button.html", + filename: "overlay/menu-button.html", + chunks: ["overlay/menu-button"], + }), + new HtmlWebpackPlugin({ + template: "./src/autofill/overlay/inline-menu/pages/list/list.html", + filename: "overlay/menu-list.html", + chunks: ["overlay/menu-list"], + }), + new HtmlWebpackPlugin({ + template: "./src/autofill/overlay/inline-menu/pages/menu-container/menu-container.html", + filename: "overlay/menu.html", + chunks: ["overlay/menu"], + }), + new HtmlWebpackPlugin({ + template: "./src/autofill/deprecated/overlay/pages/button/legacy-button.html", filename: "overlay/button.html", chunks: ["overlay/button"], }), new HtmlWebpackPlugin({ - template: "./src/autofill/overlay/pages/list/list.html", + template: "./src/autofill/deprecated/overlay/pages/list/legacy-list.html", filename: "overlay/list.html", chunks: ["overlay/list"], }), @@ -161,6 +176,8 @@ const mainConfig = { "./src/autofill/content/trigger-autofill-script-injection.ts", "content/bootstrap-autofill": "./src/autofill/content/bootstrap-autofill.ts", "content/bootstrap-autofill-overlay": "./src/autofill/content/bootstrap-autofill-overlay.ts", + "content/bootstrap-legacy-autofill-overlay": + "./src/autofill/deprecated/content/bootstrap-legacy-autofill-overlay.ts", "content/autofiller": "./src/autofill/content/autofiller.ts", "content/notificationBar": "./src/autofill/content/notification-bar.ts", "content/contextMenuHandler": "./src/autofill/content/context-menu-handler.ts", @@ -168,8 +185,16 @@ const mainConfig = { "content/fido2-content-script": "./src/autofill/fido2/content/fido2-content-script.ts", "content/fido2-page-script": "./src/autofill/fido2/content/fido2-page-script.ts", "notification/bar": "./src/autofill/notification/bar.ts", - "overlay/button": "./src/autofill/overlay/pages/button/bootstrap-autofill-overlay-button.ts", - "overlay/list": "./src/autofill/overlay/pages/list/bootstrap-autofill-overlay-list.ts", + "overlay/menu-button": + "./src/autofill/overlay/inline-menu/pages/button/bootstrap-autofill-inline-menu-button.ts", + "overlay/menu-list": + "./src/autofill/overlay/inline-menu/pages/list/bootstrap-autofill-inline-menu-list.ts", + "overlay/menu": + "./src/autofill/overlay/inline-menu/pages/menu-container/bootstrap-autofill-inline-menu-container.ts", + "overlay/button": + "./src/autofill/deprecated/overlay/pages/button/bootstrap-autofill-overlay-button.deprecated.ts", + "overlay/list": + "./src/autofill/deprecated/overlay/pages/list/bootstrap-autofill-overlay-list.deprecated.ts", "encrypt-worker": "../../libs/common/src/platform/services/cryptography/encrypt.worker.ts", "content/lp-fileless-importer": "./src/tools/content/lp-fileless-importer.ts", "content/send-on-installed-message": "./src/vault/content/send-on-installed-message.ts", diff --git a/libs/common/src/autofill/constants/index.ts b/libs/common/src/autofill/constants/index.ts index 6d5af41a17e..efbd0896428 100644 --- a/libs/common/src/autofill/constants/index.ts +++ b/libs/common/src/autofill/constants/index.ts @@ -13,13 +13,16 @@ export const EVENTS = { BLUR: "blur", CLICK: "click", FOCUS: "focus", + FOCUSIN: "focusin", + FOCUSOUT: "focusout", SCROLL: "scroll", RESIZE: "resize", DOMCONTENTLOADED: "DOMContentLoaded", LOAD: "load", MESSAGE: "message", VISIBILITYCHANGE: "visibilitychange", - FOCUSOUT: "focusout", + MOUSEENTER: "mouseenter", + MOUSELEAVE: "mouseleave", } as const; export const ClearClipboardDelay = { @@ -51,6 +54,8 @@ export const SEPARATOR_ID = "separator"; export const NOTIFICATION_BAR_LIFESPAN_MS = 150000; // 150 seconds +export const AUTOFILL_OVERLAY_HANDLE_REPOSITION = "autofill-overlay-handle-reposition-event"; + export const AutofillOverlayVisibility = { Off: 0, OnButtonClick: 1, diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index ba23b90cd22..fb4bd1f9668 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -20,6 +20,7 @@ export enum FeatureFlag { MemberAccessReport = "ac-2059-member-access-report", TwoFactorComponentRefactor = "two-factor-component-refactor", EnableTimeThreshold = "PM-5864-dollar-threshold", + InlineMenuPositioningImprovements = "inline-menu-positioning-improvements", GroupsComponentRefactor = "groups-component-refactor", ProviderClientVaultPrivacyBanner = "ac-2833-provider-client-vault-privacy-banner", VaultBulkManagementAction = "vault-bulk-management-action", @@ -54,6 +55,7 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.MemberAccessReport]: FALSE, [FeatureFlag.TwoFactorComponentRefactor]: FALSE, [FeatureFlag.EnableTimeThreshold]: FALSE, + [FeatureFlag.InlineMenuPositioningImprovements]: FALSE, [FeatureFlag.GroupsComponentRefactor]: FALSE, [FeatureFlag.ProviderClientVaultPrivacyBanner]: FALSE, [FeatureFlag.VaultBulkManagementAction]: FALSE, From c72fae5c9a40c03117ef98527fe82e5608a97cb0 Mon Sep 17 00:00:00 2001 From: Bitwarden DevOps <106330231+bitwarden-devops-bot@users.noreply.github.com> Date: Mon, 15 Jul 2024 11:09:09 -0600 Subject: [PATCH 34/57] Bumped client version(s) (#10123) --- apps/browser/package.json | 2 +- apps/browser/src/manifest.json | 2 +- apps/browser/src/manifest.v3.json | 2 +- apps/cli/package.json | 2 +- apps/desktop/package.json | 2 +- apps/desktop/src/package-lock.json | 4 ++-- apps/desktop/src/package.json | 2 +- package-lock.json | 6 +++--- 8 files changed, 11 insertions(+), 11 deletions(-) diff --git a/apps/browser/package.json b/apps/browser/package.json index f7c577e7f7f..3c8eb50f387 100644 --- a/apps/browser/package.json +++ b/apps/browser/package.json @@ -1,6 +1,6 @@ { "name": "@bitwarden/browser", - "version": "2024.7.0", + "version": "2024.7.1", "scripts": { "build": "cross-env MANIFEST_VERSION=3 webpack", "build:mv2": "webpack", diff --git a/apps/browser/src/manifest.json b/apps/browser/src/manifest.json index 1979d703641..35eb3daebfe 100644 --- a/apps/browser/src/manifest.json +++ b/apps/browser/src/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 2, "name": "__MSG_extName__", "short_name": "__MSG_appName__", - "version": "2024.7.0", + "version": "2024.7.1", "description": "__MSG_extDesc__", "default_locale": "en", "author": "Bitwarden Inc.", diff --git a/apps/browser/src/manifest.v3.json b/apps/browser/src/manifest.v3.json index c01117bfe1d..6c38af642be 100644 --- a/apps/browser/src/manifest.v3.json +++ b/apps/browser/src/manifest.v3.json @@ -3,7 +3,7 @@ "minimum_chrome_version": "102.0", "name": "__MSG_extName__", "short_name": "__MSG_appName__", - "version": "2024.7.0", + "version": "2024.7.1", "description": "__MSG_extDesc__", "default_locale": "en", "author": "Bitwarden Inc.", diff --git a/apps/cli/package.json b/apps/cli/package.json index 2822bd52ca7..2ae40a15ae4 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -1,7 +1,7 @@ { "name": "@bitwarden/cli", "description": "A secure and free password manager for all of your devices.", - "version": "2024.7.0", + "version": "2024.7.1", "keywords": [ "bitwarden", "password", diff --git a/apps/desktop/package.json b/apps/desktop/package.json index f1639dc51a1..61d4607cea0 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@bitwarden/desktop", "description": "A secure and free password manager for all of your devices.", - "version": "2024.7.1", + "version": "2024.7.2", "keywords": [ "bitwarden", "password", diff --git a/apps/desktop/src/package-lock.json b/apps/desktop/src/package-lock.json index 4ba7c6b6336..d3d99c4cfb3 100644 --- a/apps/desktop/src/package-lock.json +++ b/apps/desktop/src/package-lock.json @@ -1,12 +1,12 @@ { "name": "@bitwarden/desktop", - "version": "2024.7.1", + "version": "2024.7.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@bitwarden/desktop", - "version": "2024.7.1", + "version": "2024.7.2", "license": "GPL-3.0", "dependencies": { "@bitwarden/desktop-napi": "file:../desktop_native", diff --git a/apps/desktop/src/package.json b/apps/desktop/src/package.json index 1793642dab6..a6bd0d9ef39 100644 --- a/apps/desktop/src/package.json +++ b/apps/desktop/src/package.json @@ -2,7 +2,7 @@ "name": "@bitwarden/desktop", "productName": "Bitwarden", "description": "A secure and free password manager for all of your devices.", - "version": "2024.7.1", + "version": "2024.7.2", "author": "Bitwarden Inc. (https://bitwarden.com)", "homepage": "https://bitwarden.com", "license": "GPL-3.0", diff --git a/package-lock.json b/package-lock.json index b0681640f98..45b1bc18282 100644 --- a/package-lock.json +++ b/package-lock.json @@ -195,11 +195,11 @@ }, "apps/browser": { "name": "@bitwarden/browser", - "version": "2024.7.0" + "version": "2024.7.1" }, "apps/cli": { "name": "@bitwarden/cli", - "version": "2024.7.0", + "version": "2024.7.1", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@koa/multer": "3.0.2", @@ -235,7 +235,7 @@ }, "apps/desktop": { "name": "@bitwarden/desktop", - "version": "2024.7.1", + "version": "2024.7.2", "hasInstallScript": true, "license": "GPL-3.0" }, From bc7c6dd04ec7228cdcc4e281788f4154e1dd0d4a Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Mon, 15 Jul 2024 20:09:25 +0200 Subject: [PATCH 35/57] [PM-9710] [Bootstrap] Hide file inputs when not using bootstrap (#10095) When removing boostrap it seems the hidden attribute to the file inputs stops working. This implements a "quick" fix by just adding the tw-hidden class. --- apps/web/src/app/billing/individual/premium.component.html | 1 + .../billing/organizations/organization-plans.component.html | 3 ++- .../app/billing/shared/update-license-dialog.component.html | 1 + apps/web/src/app/billing/shared/update-license.component.html | 1 + apps/web/src/app/tools/send/add-edit.component.html | 3 ++- .../secrets-manager/settings/porting/sm-import.component.html | 3 ++- libs/importer/src/components/import.component.html | 1 + 7 files changed, 10 insertions(+), 3 deletions(-) diff --git a/apps/web/src/app/billing/individual/premium.component.html b/apps/web/src/app/billing/individual/premium.component.html index e3afa7779b8..ae95475f1c6 100644 --- a/apps/web/src/app/billing/individual/premium.component.html +++ b/apps/web/src/app/billing/individual/premium.component.html @@ -82,6 +82,7 @@ formControlName="file" (change)="setSelectedFile($event)" hidden + class="tw-hidden" /> {{ "licenseFileDesc" | i18n: "bitwarden_premium_license.json" }} 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 b9a3cc6bf05..5f34ef6cb95 100644 --- a/apps/web/src/app/billing/organizations/organization-plans.component.html +++ b/apps/web/src/app/billing/organizations/organization-plans.component.html @@ -19,12 +19,13 @@
{{ "licenseFileDesc" | i18n: "bitwarden_organization_license.json" }} diff --git a/apps/web/src/app/billing/shared/update-license-dialog.component.html b/apps/web/src/app/billing/shared/update-license-dialog.component.html index 6430c47528f..7535fe9b30b 100644 --- a/apps/web/src/app/billing/shared/update-license-dialog.component.html +++ b/apps/web/src/app/billing/shared/update-license-dialog.component.html @@ -16,6 +16,7 @@ formControlName="file" (change)="setSelectedFile($event)" hidden + class="tw-hidden" /> {{ "licenseFileDesc" | i18n: "bitwarden_premium_license.json" }} diff --git a/apps/web/src/app/billing/shared/update-license.component.html b/apps/web/src/app/billing/shared/update-license.component.html index 3cdc6fa3aeb..938179469e4 100644 --- a/apps/web/src/app/billing/shared/update-license.component.html +++ b/apps/web/src/app/billing/shared/update-license.component.html @@ -14,6 +14,7 @@ formControlName="file" (change)="setSelectedFile($event)" hidden + class="tw-hidden" /> {{ "licenseFileDesc" diff --git a/apps/web/src/app/tools/send/add-edit.component.html b/apps/web/src/app/tools/send/add-edit.component.html index cc96908eaa9..500100afd49 100644 --- a/apps/web/src/app/tools/send/add-edit.component.html +++ b/apps/web/src/app/tools/send/add-edit.component.html @@ -76,12 +76,13 @@ {{ "sendFileDesc" | i18n }} {{ "maxFileSize" | i18n }} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/settings/porting/sm-import.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/settings/porting/sm-import.component.html index 31f16111483..2254800170c 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/settings/porting/sm-import.component.html +++ b/bitwarden_license/bit-web/src/app/secrets-manager/settings/porting/sm-import.component.html @@ -11,7 +11,6 @@ {{ "acceptedFormats" | i18n }} Bitwarden (json) diff --git a/libs/importer/src/components/import.component.html b/libs/importer/src/components/import.component.html index eda999ab47e..54df0ba4d27 100644 --- a/libs/importer/src/components/import.component.html +++ b/libs/importer/src/components/import.component.html @@ -382,6 +382,7 @@ formControlName="file" (change)="setSelectedFile($event)" hidden + class="tw-hidden" /> From 154f15fa5850eba3660fba501feaea2afab44d80 Mon Sep 17 00:00:00 2001 From: Victoria League Date: Mon, 15 Jul 2024 14:29:58 -0400 Subject: [PATCH 36/57] [CL-285] Add default loading state to popup-page (#10040) --- apps/browser/src/_locales/en/messages.json | 3 ++ .../platform/popup/layout/popup-layout.mdx | 20 ++++++++ .../popup/layout/popup-layout.stories.ts | 47 ++++++++----------- .../popup/layout/popup-page.component.html | 13 ++++- .../popup/layout/popup-page.component.ts | 15 +++++- 5 files changed, 67 insertions(+), 31 deletions(-) diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index bf29ff2fd26..a89ac05e4e6 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -3628,5 +3628,8 @@ }, "addAccount": { "message": "Add account" + }, + "loading": { + "message": "Loading" } } diff --git a/apps/browser/src/platform/popup/layout/popup-layout.mdx b/apps/browser/src/platform/popup/layout/popup-layout.mdx index 6f72f325bf1..b805805ad18 100644 --- a/apps/browser/src/platform/popup/layout/popup-layout.mdx +++ b/apps/browser/src/platform/popup/layout/popup-layout.mdx @@ -44,6 +44,14 @@ page looks nice when the extension is popped out. - default - Whatever content you want in `main`. +**Inputs** + +- `loading` + - When `true`, displays a loading state overlay instead of the default content. Defaults to + `false`. +- `loadingText` + - Custom text to be applied to the loading element for screenreaders only. Defaults to "Loading". + Basic usage example: ```html @@ -137,8 +145,20 @@ When the browser extension is popped out, the "popout" button should not be pass +# Other stories + ## Centered Content +An example of how to center the default content. + + +## Loading + +An example of what the loading state looks like. + + + + diff --git a/apps/browser/src/platform/popup/layout/popup-layout.stories.ts b/apps/browser/src/platform/popup/layout/popup-layout.stories.ts index 9883a5cfb6f..f2208a8b8f5 100644 --- a/apps/browser/src/platform/popup/layout/popup-layout.stories.ts +++ b/apps/browser/src/platform/popup/layout/popup-layout.stories.ts @@ -62,27 +62,6 @@ class VaultComponent { protected data = Array.from(Array(20).keys()); } -@Component({ - selector: "generator-placeholder", - template: `
generator stuff here
`, - standalone: true, -}) -class GeneratorComponent {} - -@Component({ - selector: "send-placeholder", - template: `
send some stuff
`, - standalone: true, -}) -class SendComponent {} - -@Component({ - selector: "settings-placeholder", - template: `
change your settings
`, - standalone: true, -}) -class SettingsComponent {} - @Component({ selector: "mock-add-button", template: ` @@ -186,7 +165,7 @@ class MockVaultPagePoppedComponent {}
- +
Generator content here
`, standalone: true, @@ -196,7 +175,6 @@ class MockVaultPagePoppedComponent {} MockAddButtonComponent, MockPopoutButtonComponent, MockCurrentAccountComponent, - GeneratorComponent, ], }) class MockGeneratorPageComponent {} @@ -212,7 +190,7 @@ class MockGeneratorPageComponent {}
- +
Send content here
`, standalone: true, @@ -222,7 +200,6 @@ class MockGeneratorPageComponent {} MockAddButtonComponent, MockPopoutButtonComponent, MockCurrentAccountComponent, - SendComponent, ], }) class MockSendPageComponent {} @@ -238,7 +215,7 @@ class MockSendPageComponent {} - +
Settings content here
`, standalone: true, @@ -248,7 +225,6 @@ class MockSendPageComponent {} MockAddButtonComponent, MockPopoutButtonComponent, MockCurrentAccountComponent, - SettingsComponent, ], }) class MockSettingsPageComponent {} @@ -312,6 +288,7 @@ export default { useFactory: () => { return new I18nMockService({ back: "Back", + loading: "Loading", }); }, }, @@ -406,3 +383,19 @@ export const CenteredContent: Story = { `, }), }; + +export const Loading: Story = { + render: (args) => ({ + props: args, + template: /* HTML */ ` + + + + + Content would go here + + + + `, + }), +}; diff --git a/apps/browser/src/platform/popup/layout/popup-page.component.html b/apps/browser/src/platform/popup/layout/popup-page.component.html index b3dcd626ae3..87f91e781a7 100644 --- a/apps/browser/src/platform/popup/layout/popup-page.component.html +++ b/apps/browser/src/platform/popup/layout/popup-page.component.html @@ -1,7 +1,16 @@ -
-
+
+
+ + +
diff --git a/apps/browser/src/platform/popup/layout/popup-page.component.ts b/apps/browser/src/platform/popup/layout/popup-page.component.ts index 1223a6f4188..97a67fc852c 100644 --- a/apps/browser/src/platform/popup/layout/popup-page.component.ts +++ b/apps/browser/src/platform/popup/layout/popup-page.component.ts @@ -1,4 +1,7 @@ -import { Component } from "@angular/core"; +import { CommonModule } from "@angular/common"; +import { Component, Input, inject } from "@angular/core"; + +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @Component({ selector: "popup-page", @@ -7,5 +10,13 @@ import { Component } from "@angular/core"; host: { class: "tw-h-full tw-flex tw-flex-col tw-flex-1 tw-overflow-y-auto", }, + imports: [CommonModule], }) -export class PopupPageComponent {} +export class PopupPageComponent { + protected i18nService = inject(I18nService); + + @Input() loading = false; + + /** Accessible loading label for the spinner. Defaults to "loading" */ + @Input() loadingText?: string = this.i18nService.t("loading"); +} From d852a5ff07b6a9d9a9a18a809fc7da3fbbf68160 Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Tue, 16 Jul 2024 12:59:04 +0200 Subject: [PATCH 37/57] [PM-97089] [Bootstrap] Convert remaining sr-only to tw-sr-only (#10094) We're still using sr-only in multiple places. This is a bootstrap utility class and needs to be migrated to tw-sr-only. --- .../manage/group-add-edit.component.html | 2 +- .../organizations/manage/groups.component.html | 2 +- .../organizations/manage/new-groups.component.html | 2 +- .../organizations/members/members.component.html | 2 +- .../access-selector/access-selector.component.html | 2 +- .../accept-family-sponsorship.component.html | 2 +- .../providers/providers.component.html | 4 ++-- .../login-decryption-options.component.html | 2 +- .../accept-organization.component.html | 2 +- .../auth/settings/two-factor-setup.component.html | 4 ++-- .../src/app/auth/verify-email-token.component.html | 2 +- .../trial-billing-step.component.html | 2 +- .../organization-subscription-cloud.component.html | 2 +- ...ganization-subscription-selfhost.component.html | 6 +++--- apps/web/src/app/tools/generator.component.html | 2 +- .../pages/exposed-passwords-report.component.html | 4 ++-- .../inactive-two-factor-report.component.html | 6 +++--- .../pages/reused-passwords-report.component.html | 6 +++--- .../pages/unsecured-websites-report.component.html | 6 +++--- .../pages/weak-passwords-report.component.html | 6 +++--- apps/web/src/app/tools/send/send.component.html | 14 +++++++------- .../vault-items/vault-cipher-row.component.html | 4 ++-- .../add-edit-custom-fields.component.html | 2 +- .../individual-vault/attachments.component.html | 4 ++-- .../vault/individual-vault/vault.component.html | 2 +- .../src/app/vault/org-vault/vault.component.html | 2 +- .../domain-verification.component.html | 2 +- .../providers/clients/clients.component.html | 2 +- .../providers/manage/people.component.html | 4 ++-- .../providers/manage/user-add-edit.component.html | 2 +- .../providers/setup/setup-provider.component.html | 2 +- .../providers/setup/setup.component.html | 2 +- .../bit-web/src/app/auth/sso/sso.component.ts | 2 +- .../clients/manage-clients.component.html | 2 +- .../provider-payment-method.component.html | 2 +- .../provider-subscription.component.html | 2 +- 36 files changed, 58 insertions(+), 58 deletions(-) diff --git a/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.html b/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.html index eaf10405dbf..3151c303ec9 100644 --- a/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.html +++ b/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.html @@ -13,7 +13,7 @@ title="{{ 'loading' | i18n }}" aria-hidden="true" > - {{ "loading" | i18n }} + {{ "loading" | i18n }}
diff --git a/apps/web/src/app/admin-console/organizations/manage/groups.component.html b/apps/web/src/app/admin-console/organizations/manage/groups.component.html index 1a1a7cdb904..2ebafb38fc9 100644 --- a/apps/web/src/app/admin-console/organizations/manage/groups.component.html +++ b/apps/web/src/app/admin-console/organizations/manage/groups.component.html @@ -16,7 +16,7 @@ title="{{ 'loading' | i18n }}" aria-hidden="true" > - {{ "loading" | i18n }} + {{ "loading" | i18n }}

{{ "noGroupsInList" | i18n }}

diff --git a/apps/web/src/app/admin-console/organizations/manage/new-groups.component.html b/apps/web/src/app/admin-console/organizations/manage/new-groups.component.html index 3e659e5b6a8..1254d48cc76 100644 --- a/apps/web/src/app/admin-console/organizations/manage/new-groups.component.html +++ b/apps/web/src/app/admin-console/organizations/manage/new-groups.component.html @@ -16,7 +16,7 @@ title="{{ 'loading' | i18n }}" aria-hidden="true" > - {{ "loading" | i18n }} + {{ "loading" | i18n }}

{{ "noGroupsInList" | i18n }}

diff --git a/apps/web/src/app/admin-console/organizations/members/members.component.html b/apps/web/src/app/admin-console/organizations/members/members.component.html index 64e4b345476..f5b824face6 100644 --- a/apps/web/src/app/admin-console/organizations/members/members.component.html +++ b/apps/web/src/app/admin-console/organizations/members/members.component.html @@ -52,7 +52,7 @@ title="{{ 'loading' | i18n }}" aria-hidden="true" > - {{ "loading" | i18n }} + {{ "loading" | i18n }}

{{ "noMembersInList" | i18n }}

diff --git a/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.component.html b/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.component.html index 9077bd747fd..aff5d25ee08 100644 --- a/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.component.html +++ b/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.component.html @@ -85,7 +85,7 @@ -
diff --git a/apps/web/src/app/admin-console/organizations/sponsorships/accept-family-sponsorship.component.html b/apps/web/src/app/admin-console/organizations/sponsorships/accept-family-sponsorship.component.html index e7eb29a3ac7..7640e1c7366 100644 --- a/apps/web/src/app/admin-console/organizations/sponsorships/accept-family-sponsorship.component.html +++ b/apps/web/src/app/admin-console/organizations/sponsorships/accept-family-sponsorship.component.html @@ -7,7 +7,7 @@ title="{{ 'loading' | i18n }}" aria-hidden="true" > - {{ "loading" | i18n }} + {{ "loading" | i18n }}

diff --git a/apps/web/src/app/admin-console/providers/providers.component.html b/apps/web/src/app/admin-console/providers/providers.component.html index d07342c85c2..560c164415c 100644 --- a/apps/web/src/app/admin-console/providers/providers.component.html +++ b/apps/web/src/app/admin-console/providers/providers.component.html @@ -3,7 +3,7 @@

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

@@ -20,7 +20,7 @@ title="{{ 'providerIsDisabled' | i18n }}" aria-hidden="true" > - {{ "providerIsDisabled" | i18n }} + {{ "providerIsDisabled" | i18n }} diff --git a/apps/web/src/app/auth/login/login-decryption-options/login-decryption-options.component.html b/apps/web/src/app/auth/login/login-decryption-options/login-decryption-options.component.html index ed59cc12388..615edb82d0c 100644 --- a/apps/web/src/app/auth/login/login-decryption-options/login-decryption-options.component.html +++ b/apps/web/src/app/auth/login/login-decryption-options/login-decryption-options.component.html @@ -13,7 +13,7 @@ title="{{ 'loading' | i18n }}" aria-hidden="true" > - {{ "loading" | i18n }} + {{ "loading" | i18n }}

diff --git a/apps/web/src/app/auth/organization-invite/accept-organization.component.html b/apps/web/src/app/auth/organization-invite/accept-organization.component.html index 04258e7a46a..88eaa37e8d2 100644 --- a/apps/web/src/app/auth/organization-invite/accept-organization.component.html +++ b/apps/web/src/app/auth/organization-invite/accept-organization.component.html @@ -7,7 +7,7 @@ title="{{ 'loading' | i18n }}" aria-hidden="true" > - {{ "loading" | i18n }} + {{ "loading" | i18n }}

diff --git a/apps/web/src/app/auth/settings/two-factor-setup.component.html b/apps/web/src/app/auth/settings/two-factor-setup.component.html index 33265e91f78..3595d9a7dcb 100644 --- a/apps/web/src/app/auth/settings/two-factor-setup.component.html +++ b/apps/web/src/app/auth/settings/two-factor-setup.component.html @@ -39,7 +39,7 @@ title="{{ 'loading' | i18n }}" aria-hidden="true" > - {{ "loading" | i18n }} + {{ "loading" | i18n }} @@ -64,7 +64,7 @@ title="{{ 'enabled' | i18n }}" aria-hidden="true" > - {{ "enabled" | i18n }} + {{ "enabled" | i18n }} diff --git a/apps/web/src/app/auth/verify-email-token.component.html b/apps/web/src/app/auth/verify-email-token.component.html index 9e0aad8b384..39a7d4524f8 100644 --- a/apps/web/src/app/auth/verify-email-token.component.html +++ b/apps/web/src/app/auth/verify-email-token.component.html @@ -7,7 +7,7 @@ title="{{ 'loading' | i18n }}" aria-hidden="true" > - {{ "loading" | i18n }} + {{ "loading" | i18n }}

diff --git a/apps/web/src/app/billing/accounts/trial-initiation/trial-billing-step.component.html b/apps/web/src/app/billing/accounts/trial-initiation/trial-billing-step.component.html index fb7c774d6db..71a4ff119c2 100644 --- a/apps/web/src/app/billing/accounts/trial-initiation/trial-billing-step.component.html +++ b/apps/web/src/app/billing/accounts/trial-initiation/trial-billing-step.component.html @@ -4,7 +4,7 @@ title="{{ 'loading' | i18n }}" aria-hidden="true" > - {{ "loading" | i18n }} + {{ "loading" | i18n }}
- {{ "loading" | i18n }} + {{ "loading" | i18n }} - {{ "loading" | i18n }} + {{ "loading" | i18n }} - {{ "licensePaidFeaturesHelp" | i18n }} + {{ "licensePaidFeaturesHelp" | i18n }}
@@ -84,7 +84,7 @@ rel="noreferrer" > - {{ "billingSyncHelp" | i18n }} + {{ "billingSyncHelp" | i18n }} diff --git a/apps/web/src/app/tools/generator.component.html b/apps/web/src/app/tools/generator.component.html index 4be83c3edb5..f52d1f020d3 100644 --- a/apps/web/src/app/tools/generator.component.html +++ b/apps/web/src/app/tools/generator.component.html @@ -126,7 +126,7 @@ [value]="passwordOptions.length" />
@@ -189,7 +189,7 @@ title="{{ 'loading' | i18n }}" aria-hidden="true" > - {{ "loading" | i18n }} + {{ "loading" | i18n }} diff --git a/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.html b/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.html index d4ce1e79ba5..0e515a307c6 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.html +++ b/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.html @@ -34,7 +34,7 @@ title="{{ 'attachments' | i18n }}" aria-hidden="true" > - {{ "attachments" | i18n }} + {{ "attachments" | i18n }} - {{ "attachmentsNeedFix" | i18n }} + {{ "attachmentsNeedFix" | i18n }} diff --git a/apps/web/src/app/vault/individual-vault/add-edit-custom-fields.component.html b/apps/web/src/app/vault/individual-vault/add-edit-custom-fields.component.html index dfba89e9620..1c2a75737e0 100644 --- a/apps/web/src/app/vault/individual-vault/add-edit-custom-fields.component.html +++ b/apps/web/src/app/vault/individual-vault/add-edit-custom-fields.component.html @@ -161,7 +161,7 @@
- + {{ "maxFileSize" | i18n }}
diff --git a/apps/web/src/app/vault/individual-vault/vault.component.html b/apps/web/src/app/vault/individual-vault/vault.component.html index fe1a97aff1d..f0be76018f7 100644 --- a/apps/web/src/app/vault/individual-vault/vault.component.html +++ b/apps/web/src/app/vault/individual-vault/vault.component.html @@ -65,7 +65,7 @@ title="{{ 'loading' | i18n }}" aria-hidden="true" > - {{ "loading" | i18n }} + {{ "loading" | i18n }}
diff --git a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-verification.component.html b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-verification.component.html index 1d71deca122..dee663eb199 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-verification.component.html +++ b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-verification.component.html @@ -10,7 +10,7 @@ title="{{ 'loading' | i18n }}" aria-hidden="true" > - {{ "loading" | i18n }} + {{ "loading" | i18n }} diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/clients/clients.component.html b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/clients.component.html index 4d4c7f11076..220a2214600 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/clients/clients.component.html +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/clients.component.html @@ -21,7 +21,7 @@ title="{{ 'loading' | i18n }}" aria-hidden="true" > - {{ "loading" | i18n }} + {{ "loading" | i18n }}
diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/user-add-edit.component.html b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/user-add-edit.component.html index 11f6ae07d83..78d80d005c9 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/user-add-edit.component.html +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/user-add-edit.component.html @@ -27,7 +27,7 @@ title="{{ 'loading' | i18n }}" aria-hidden="true" > - {{ "loading" | i18n }} + {{ "loading" | i18n }} 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 0fd6725304c..3b81d0564c9 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 @@ -7,7 +7,7 @@ title="{{ 'loading' | i18n }}" aria-hidden="true" > - {{ "loading" | i18n }} + {{ "loading" | i18n }}
diff --git a/libs/auth/src/angular/input-password/input-password.component.ts b/libs/auth/src/angular/input-password/input-password.component.ts index ed77e17da1a..7b5651492e1 100644 --- a/libs/auth/src/angular/input-password/input-password.component.ts +++ b/libs/auth/src/angular/input-password/input-password.component.ts @@ -2,6 +2,10 @@ import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core"; import { ReactiveFormsModule, FormBuilder, Validators } from "@angular/forms"; import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { + PasswordStrengthScore, + PasswordStrengthV2Component, +} from "@bitwarden/angular/tools/password-strength/password-strength-v2.component"; import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options"; @@ -40,6 +44,7 @@ import { PasswordInputResult } from "./password-input-result"; ReactiveFormsModule, SharedModule, PasswordCalloutComponent, + PasswordStrengthV2Component, JslibModule, ], }) @@ -56,7 +61,7 @@ export class InputPasswordComponent implements OnInit { protected minPasswordLength = Utils.minimumPasswordLength; protected minPasswordMsg = ""; - protected passwordStrengthResult: any; + protected passwordStrengthScore: PasswordStrengthScore; protected showErrorSummary = false; protected showPassword = false; @@ -112,8 +117,8 @@ export class InputPasswordComponent implements OnInit { } } - getPasswordStrengthResult(result: any) { - this.passwordStrengthResult = result; + getPasswordStrengthScore(score: PasswordStrengthScore) { + this.passwordStrengthScore = score; } protected submit = async () => { @@ -147,7 +152,7 @@ export class InputPasswordComponent implements OnInit { if ( this.masterPasswordPolicyOptions != null && !this.policyService.evaluateMasterPassword( - this.passwordStrengthResult.score, + this.passwordStrengthScore, password, this.masterPasswordPolicyOptions, ) From aa8c5b15165190f08d2e6b560e719cf1a64299be Mon Sep 17 00:00:00 2001 From: rr-bw <102181210+rr-bw@users.noreply.github.com> Date: Tue, 16 Jul 2024 09:36:05 -0700 Subject: [PATCH 43/57] make minimumLength msg update if mp policy is enforced (#10105) --- libs/angular/src/auth/components/change-password.component.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/libs/angular/src/auth/components/change-password.component.ts b/libs/angular/src/auth/components/change-password.component.ts index 5e9b31138e0..d6c0ec92710 100644 --- a/libs/angular/src/auth/components/change-password.component.ts +++ b/libs/angular/src/auth/components/change-password.component.ts @@ -62,6 +62,10 @@ export class ChangePasswordComponent implements OnInit, OnDestroy { (enforcedPasswordPolicyOptions) => (this.enforcedPolicyOptions ??= enforcedPasswordPolicyOptions), ); + + if (this.enforcedPolicyOptions?.minLength) { + this.minimumLength = this.enforcedPolicyOptions.minLength; + } } ngOnDestroy(): void { From dbc9b9c90be15c63875be180a4ea3ace88c4a0e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20=C3=85berg?= Date: Tue, 16 Jul 2024 19:39:41 +0200 Subject: [PATCH 44/57] PM-4877: Only allow replacing passkeys for the same userhandle (#9804) * Initial draft * small cleanup * show vaul items without passkeys * Refactored a bit * tests run for me? * Fixed platform test * null and undefined * lint --- .../browser-fido2-user-interface.service.ts | 3 +++ .../popup/components/fido2/fido2.component.ts | 20 +++++++++++++++++-- ...ido2-user-interface.service.abstraction.ts | 5 +++++ .../fido2/fido2-authenticator.service.spec.ts | 1 + .../fido2/fido2-authenticator.service.ts | 1 + 5 files changed, 28 insertions(+), 2 deletions(-) diff --git a/apps/browser/src/vault/fido2/browser-fido2-user-interface.service.ts b/apps/browser/src/vault/fido2/browser-fido2-user-interface.service.ts index d4ad7209b79..df4f184f7ff 100644 --- a/apps/browser/src/vault/fido2/browser-fido2-user-interface.service.ts +++ b/apps/browser/src/vault/fido2/browser-fido2-user-interface.service.ts @@ -65,6 +65,7 @@ export type BrowserFido2Message = { sessionId: string } & ( type: "ConfirmNewCredentialRequest"; credentialName: string; userName: string; + userHandle: string; userVerification: boolean; fallbackSupported: boolean; rpId: string; @@ -242,6 +243,7 @@ export class BrowserFido2UserInterfaceSession implements Fido2UserInterfaceSessi async confirmNewCredential({ credentialName, userName, + userHandle, userVerification, rpId, }: NewCredentialParams): Promise<{ cipherId: string; userVerified: boolean }> { @@ -250,6 +252,7 @@ export class BrowserFido2UserInterfaceSession implements Fido2UserInterfaceSessi sessionId: this.sessionId, credentialName, userName, + userHandle, userVerification, fallbackSupported: this.fallbackSupported, rpId, diff --git a/apps/browser/src/vault/popup/components/fido2/fido2.component.ts b/apps/browser/src/vault/popup/components/fido2/fido2.component.ts index 752a9100721..3a8c69cba95 100644 --- a/apps/browser/src/vault/popup/components/fido2/fido2.component.ts +++ b/apps/browser/src/vault/popup/components/fido2/fido2.component.ts @@ -143,8 +143,10 @@ export class Fido2Component implements OnInit, OnDestroy { this.ciphers = (await this.cipherService.getAllDecrypted()).filter( (cipher) => cipher.type === CipherType.Login && !cipher.isDeleted, ); - this.displayedCiphers = this.ciphers.filter((cipher) => - cipher.login.matchesUri(this.url, equivalentDomains), + this.displayedCiphers = this.ciphers.filter( + (cipher) => + cipher.login.matchesUri(this.url, equivalentDomains) && + this.hasNoOtherPasskeys(cipher, message.userHandle), ); if (this.displayedCiphers.length > 0) { @@ -405,4 +407,18 @@ export class Fido2Component implements OnInit, OnDestroy { ...msg, }); } + + /** + * This methods returns true if a cipher either has no passkeys, or has a passkey matching with userHandle + * @param userHandle + */ + private hasNoOtherPasskeys(cipher: CipherView, userHandle: string): boolean { + if (cipher.login.fido2Credentials == null || cipher.login.fido2Credentials.length === 0) { + return true; + } + + return cipher.login.fido2Credentials.some((passkey) => { + passkey.userHandle === userHandle; + }); + } } diff --git a/libs/common/src/platform/abstractions/fido2/fido2-user-interface.service.abstraction.ts b/libs/common/src/platform/abstractions/fido2/fido2-user-interface.service.abstraction.ts index aba18f9ecd1..9882febdd39 100644 --- a/libs/common/src/platform/abstractions/fido2/fido2-user-interface.service.abstraction.ts +++ b/libs/common/src/platform/abstractions/fido2/fido2-user-interface.service.abstraction.ts @@ -12,6 +12,11 @@ export interface NewCredentialParams { */ userName: string; + /** + * The userhandle (userid) of the user. + */ + userHandle: string; + /** * Whether or not the user must be verified before completing the operation. */ diff --git a/libs/common/src/platform/services/fido2/fido2-authenticator.service.spec.ts b/libs/common/src/platform/services/fido2/fido2-authenticator.service.spec.ts index 5da67f807b7..202381c5ead 100644 --- a/libs/common/src/platform/services/fido2/fido2-authenticator.service.spec.ts +++ b/libs/common/src/platform/services/fido2/fido2-authenticator.service.spec.ts @@ -215,6 +215,7 @@ describe("FidoAuthenticatorService", () => { expect(userInterfaceSession.confirmNewCredential).toHaveBeenCalledWith({ credentialName: params.rpEntity.name, userName: params.userEntity.name, + userHandle: Fido2Utils.bufferToString(params.userEntity.id), userVerification, rpId: params.rpEntity.id, } as NewCredentialParams); diff --git a/libs/common/src/platform/services/fido2/fido2-authenticator.service.ts b/libs/common/src/platform/services/fido2/fido2-authenticator.service.ts index 47d76897a3b..3464154b9cc 100644 --- a/libs/common/src/platform/services/fido2/fido2-authenticator.service.ts +++ b/libs/common/src/platform/services/fido2/fido2-authenticator.service.ts @@ -112,6 +112,7 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr const response = await userInterfaceSession.confirmNewCredential({ credentialName: params.rpEntity.name, userName: params.userEntity.name, + userHandle: Fido2Utils.bufferToString(params.userEntity.id), userVerification: params.requireUserVerification, rpId: params.rpEntity.id, }); From 90de9dd07afeedb7bb4b362c2dcfef4b52921469 Mon Sep 17 00:00:00 2001 From: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> Date: Tue, 16 Jul 2024 23:12:46 +0200 Subject: [PATCH 45/57] Create browsers SendV2 component (#10136) Co-authored-by: Daniel James Smith --- apps/browser/src/_locales/en/messages.json | 8 +++ apps/browser/src/popup/app-routing.module.ts | 6 +-- .../tools/popup/send/send-v2.component.html | 21 ++++++++ .../src/tools/popup/send/send-v2.component.ts | 52 +++++++++++++++++++ 4 files changed, 84 insertions(+), 3 deletions(-) create mode 100644 apps/browser/src/tools/popup/send/send-v2.component.html create mode 100644 apps/browser/src/tools/popup/send/send-v2.component.ts diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 6cebe0e2319..69c3a525f93 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -2765,6 +2765,14 @@ "deviceTrusted": { "message": "Device trusted" }, + "sendsNoItemsTitle": { + "message": "No active Sends", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendsNoItemsMessage": { + "message": "Use Send to securely share encrypted information with anyone.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Input is required." }, diff --git a/apps/browser/src/popup/app-routing.module.ts b/apps/browser/src/popup/app-routing.module.ts index 8645cb797bd..12d92249fed 100644 --- a/apps/browser/src/popup/app-routing.module.ts +++ b/apps/browser/src/popup/app-routing.module.ts @@ -48,6 +48,7 @@ import { PasswordGeneratorHistoryComponent } from "../tools/popup/generator/pass import { SendAddEditComponent } from "../tools/popup/send/send-add-edit.component"; import { SendGroupingsComponent } from "../tools/popup/send/send-groupings.component"; import { SendTypeComponent } from "../tools/popup/send/send-type.component"; +import { SendV2Component } from "../tools/popup/send/send-v2.component"; import { AboutPageV2Component } from "../tools/popup/settings/about-page/about-page-v2.component"; import { AboutPageComponent } from "../tools/popup/settings/about-page/about-page.component"; import { MoreFromBitwardenPageV2Component } from "../tools/popup/settings/about-page/more-from-bitwarden-page-v2.component"; @@ -450,12 +451,11 @@ const routes: Routes = [ canActivate: [AuthGuard], data: { state: "tabs_settings" }, }), - { + ...extensionRefreshSwap(SendGroupingsComponent, SendV2Component, { path: "send", - component: SendGroupingsComponent, canActivate: [AuthGuard], data: { state: "tabs_send" }, - }, + }), ], }), { diff --git a/apps/browser/src/tools/popup/send/send-v2.component.html b/apps/browser/src/tools/popup/send/send-v2.component.html new file mode 100644 index 00000000000..3499f8c32ef --- /dev/null +++ b/apps/browser/src/tools/popup/send/send-v2.component.html @@ -0,0 +1,21 @@ + + + + + + + + + + +
+ + {{ "sendsNoItemsTitle" | i18n }} + {{ "sendsNoItemsMessage" | i18n }} + + +
+
diff --git a/apps/browser/src/tools/popup/send/send-v2.component.ts b/apps/browser/src/tools/popup/send/send-v2.component.ts new file mode 100644 index 00000000000..fba14b762b1 --- /dev/null +++ b/apps/browser/src/tools/popup/send/send-v2.component.ts @@ -0,0 +1,52 @@ +import { CommonModule } from "@angular/common"; +import { Component, OnDestroy, OnInit } from "@angular/core"; +import { RouterLink } from "@angular/router"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { SendType } from "@bitwarden/common/tools/send/enums/send-type"; +import { ButtonModule, NoItemsModule } from "@bitwarden/components"; +import { NoSendsIcon, NewSendDropdownComponent } from "@bitwarden/send-ui"; + +import { CurrentAccountComponent } from "../../../auth/popup/account-switching/current-account.component"; +import { PopOutComponent } from "../../../platform/popup/components/pop-out.component"; +import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component"; +import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component"; + +enum SendsListState { + Empty, +} + +@Component({ + templateUrl: "send-v2.component.html", + standalone: true, + imports: [ + PopupPageComponent, + PopupHeaderComponent, + PopOutComponent, + CurrentAccountComponent, + NoItemsModule, + JslibModule, + CommonModule, + ButtonModule, + RouterLink, + NewSendDropdownComponent, + ], +}) +export class SendV2Component implements OnInit, OnDestroy { + sendType = SendType; + + /** Visual state of the Sends list */ + protected sendsListState: SendsListState | null = null; + + protected noItemIcon = NoSendsIcon; + + protected SendsListStateEnum = SendsListState; + + constructor() { + this.sendsListState = SendsListState.Empty; + } + + ngOnInit(): void {} + + ngOnDestroy(): void {} +} From 7dc41c0c346266e24be96cda46e5ef129f636d3e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 17 Jul 2024 14:10:30 +1000 Subject: [PATCH 46/57] [deps] AC: Update webpack to v5.93.0 (#9799) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index bae2ec44e9d..ef366f224d5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -183,7 +183,7 @@ "url": "0.11.3", "util": "0.12.5", "wait-on": "7.2.0", - "webpack": "5.92.0", + "webpack": "5.93.0", "webpack-cli": "5.1.4", "webpack-dev-server": "5.0.4", "webpack-node-externals": "3.0.0" @@ -39347,9 +39347,9 @@ } }, "node_modules/webpack": { - "version": "5.92.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.92.0.tgz", - "integrity": "sha512-Bsw2X39MYIgxouNATyVpCNVWBCuUwDgWtN78g6lSdPJRLaQ/PUVm/oXcaRAyY/sMFoKFQrsPeqvTizWtq7QPCA==", + "version": "5.93.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.93.0.tgz", + "integrity": "sha512-Y0m5oEY1LRuwly578VqluorkXbvXKh7U3rLoQCEO04M97ScRr44afGVkI0FQFsXzysk5OgFAxjZAb9rsGQVihA==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 51afc8f48b2..ed8ebcef2f6 100644 --- a/package.json +++ b/package.json @@ -145,7 +145,7 @@ "url": "0.11.3", "util": "0.12.5", "wait-on": "7.2.0", - "webpack": "5.92.0", + "webpack": "5.93.0", "webpack-cli": "5.1.4", "webpack-dev-server": "5.0.4", "webpack-node-externals": "3.0.0" From a1c5cc6dbf25ead9ac55545eed335e33cd906fc7 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Wed, 17 Jul 2024 14:13:03 +0200 Subject: [PATCH 47/57] Fix key rotation being broken due to master key validation (#10135) --- .../app/auth/settings/change-password.component.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/apps/web/src/app/auth/settings/change-password.component.ts b/apps/web/src/app/auth/settings/change-password.component.ts index aa27588691f..d8cd59435f4 100644 --- a/apps/web/src/app/auth/settings/change-password.component.ts +++ b/apps/web/src/app/auth/settings/change-password.component.ts @@ -16,7 +16,9 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; +import { HashPurpose } from "@bitwarden/common/platform/enums"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; +import { UserId } from "@bitwarden/common/types/guid"; import { MasterKey, UserKey } from "@bitwarden/common/types/key"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; @@ -178,6 +180,13 @@ export class ChangePasswordComponent extends BaseChangePasswordComponent { await this.kdfConfigService.getKdfConfig(), ); + const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(map((a) => a?.id))); + const newLocalKeyHash = await this.cryptoService.hashMasterKey( + this.masterPassword, + newMasterKey, + HashPurpose.LocalAuthorization, + ); + const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(masterKey); if (userKey == null) { this.platformUtilsService.showToast( @@ -199,7 +208,10 @@ export class ChangePasswordComponent extends BaseChangePasswordComponent { try { if (this.rotateUserKey) { - this.formPromise = this.apiService.postPassword(request).then(() => { + this.formPromise = this.apiService.postPassword(request).then(async () => { + // we need to save this for local masterkey verification during rotation + await this.masterPasswordService.setMasterKeyHash(newLocalKeyHash, userId as UserId); + await this.masterPasswordService.setMasterKey(newMasterKey, userId as UserId); return this.updateKey(); }); } else { From 83d141c914006fce1c3bae55f0a2f0ab65e1b273 Mon Sep 17 00:00:00 2001 From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> Date: Wed, 17 Jul 2024 09:11:42 -0500 Subject: [PATCH 48/57] [PM-8803] Edit Custom Fields (#10054) * initial add of custom fields * add fields for custom field * integrate custom field into cipher form service for text fields * add hidden field type * add boolean custom field * add linked option type * add testids for automated testing * add edit option for each custom field * update dialog component name to match add/edit nature * add delete button for fields * initial add of drag and drop * collect tailwind styles from vault components * add drag and drop functionality with announcement * add reorder via keyboard * update tests to match functionality * account for partial edit of custom fields * fix change detection for new fields * add label's to the edit/reorder translations * update dynamic heading to be inline * add validation/required for field label * disable toggle button on hidden fields when the user cannot view passwords * remove the need for passing `updatedCipherView` by only using a single instance of `CustomFieldsComponent` * lint fix * use bitLink styles rather than manually defining tailwind classes * use submit action, no duplicated button and allows for form submission via enter * add documentation for `newField` --- apps/browser/src/_locales/en/messages.json | 100 +++++ apps/browser/tailwind.config.js | 1 + .../src/cipher-form/cipher-form-container.ts | 2 + .../additional-options-section.component.html | 13 +- ...ditional-options-section.component.spec.ts | 20 +- .../additional-options-section.component.ts | 32 +- .../components/cipher-form.component.ts | 1 + ...dd-edit-custom-field-dialog.component.html | 48 +++ ...edit-custom-field-dialog.component.spec.ts | 72 ++++ .../add-edit-custom-field-dialog.component.ts | 120 ++++++ .../custom-fields.component.html | 111 ++++++ .../custom-fields.component.spec.ts | 373 ++++++++++++++++++ .../custom-fields/custom-fields.component.ts | 334 ++++++++++++++++ 13 files changed, 1223 insertions(+), 4 deletions(-) create mode 100644 libs/vault/src/cipher-form/components/custom-fields/add-edit-custom-field-dialog/add-edit-custom-field-dialog.component.html create mode 100644 libs/vault/src/cipher-form/components/custom-fields/add-edit-custom-field-dialog/add-edit-custom-field-dialog.component.spec.ts create mode 100644 libs/vault/src/cipher-form/components/custom-fields/add-edit-custom-field-dialog/add-edit-custom-field-dialog.component.ts create mode 100644 libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.html create mode 100644 libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.spec.ts create mode 100644 libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.ts diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 69c3a525f93..5b306fdb2a7 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -3642,5 +3642,105 @@ }, "loading": { "message": "Loading" + }, + "addField": { + "message": "Add field" + }, + "add": { + "message": "Add" + }, + "fieldType": { + "message": "Field type" + }, + "fieldLabel": { + "message": "Field label" + }, + "textHelpText": { + "message": "Use text fields for data like security questions" + }, + "hiddenHelpText": { + "message": "Use hidden fields for sensitive data like a password" + }, + "checkBoxHelpText":{ + "message": "Use checkboxes if you'd like to auto-fill a form's checkbox, like a remember email" + }, + "linkedHelpText": { + "message": "Use a linked field when you are experiencing auto-fill issues for a specific website." + }, + "linkedLabelHelpText": { + "message": "Enter the the field's html id, name, aria-label, or placeholder." + }, + "editField": { + "message": "Edit field" + }, + "editFieldLabel": { + "message": "Edit $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "deleteCustomField": { + "message": "Delete $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "fieldAdded": { + "message": "$LABEL$ added", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderToggleButton": { + "message": "Reorder $LABEL$. Use arrow key to move item up or down.", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderFieldUp":{ + "message": "$LABEL$ moved up, position $INDEX$ of $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "reorderFieldDown":{ + "message": "$LABEL$ moved down, position $INDEX$ of $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } } } diff --git a/apps/browser/tailwind.config.js b/apps/browser/tailwind.config.js index db1dd55694e..c0baf274a23 100644 --- a/apps/browser/tailwind.config.js +++ b/apps/browser/tailwind.config.js @@ -6,6 +6,7 @@ config.content = [ "../../libs/components/src/**/*.{html,ts}", "../../libs/auth/src/**/*.{html,ts}", "../../libs/angular/src/**/*.{html,ts}", + "../../libs/vault/src/**/*.{html,ts}", ]; module.exports = config; diff --git a/libs/vault/src/cipher-form/cipher-form-container.ts b/libs/vault/src/cipher-form/cipher-form-container.ts index a002e39d3e0..9655b70bbbd 100644 --- a/libs/vault/src/cipher-form/cipher-form-container.ts +++ b/libs/vault/src/cipher-form/cipher-form-container.ts @@ -3,6 +3,7 @@ import { CipherFormConfig } from "@bitwarden/vault"; import { AdditionalOptionsSectionComponent } from "./components/additional-options/additional-options-section.component"; import { CardDetailsSectionComponent } from "./components/card-details-section/card-details-section.component"; +import { CustomFieldsComponent } from "./components/custom-fields/custom-fields.component"; import { IdentitySectionComponent } from "./components/identity/identity.component"; import { ItemDetailsSectionComponent } from "./components/item-details/item-details-section.component"; @@ -15,6 +16,7 @@ export type CipherForm = { additionalOptions?: AdditionalOptionsSectionComponent["additionalOptionsForm"]; cardDetails?: CardDetailsSectionComponent["cardDetailsForm"]; identityDetails?: IdentitySectionComponent["identityForm"]; + customFields?: CustomFieldsComponent["customFieldsForm"]; }; /** diff --git a/libs/vault/src/cipher-form/components/additional-options/additional-options-section.component.html b/libs/vault/src/cipher-form/components/additional-options/additional-options-section.component.html index d9c3a002048..9f162cb25e8 100644 --- a/libs/vault/src/cipher-form/components/additional-options/additional-options-section.component.html +++ b/libs/vault/src/cipher-form/components/additional-options/additional-options-section.component.html @@ -13,8 +13,17 @@ {{ "passwordPrompt" | i18n }} - + - + diff --git a/libs/vault/src/cipher-form/components/additional-options/additional-options-section.component.spec.ts b/libs/vault/src/cipher-form/components/additional-options/additional-options-section.component.spec.ts index 71f8c4f197b..d488fc9db91 100644 --- a/libs/vault/src/cipher-form/components/additional-options/additional-options-section.component.spec.ts +++ b/libs/vault/src/cipher-form/components/additional-options/additional-options-section.component.spec.ts @@ -1,3 +1,4 @@ +import { Component } from "@angular/core"; import { ComponentFixture, TestBed } from "@angular/core/testing"; import { mock, MockProxy } from "jest-mock-extended"; import { BehaviorSubject } from "rxjs"; @@ -7,9 +8,17 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { PasswordRepromptService } from "../../../services/password-reprompt.service"; import { CipherFormContainer } from "../../cipher-form-container"; +import { CustomFieldsComponent } from "../custom-fields/custom-fields.component"; import { AdditionalOptionsSectionComponent } from "./additional-options-section.component"; +@Component({ + standalone: true, + selector: "vault-custom-fields", + template: "", +}) +class MockCustomFieldsComponent {} + describe("AdditionalOptionsSectionComponent", () => { let component: AdditionalOptionsSectionComponent; let fixture: ComponentFixture; @@ -31,7 +40,16 @@ describe("AdditionalOptionsSectionComponent", () => { { provide: PasswordRepromptService, useValue: passwordRepromptService }, { provide: I18nService, useValue: mock() }, ], - }).compileComponents(); + }) + .overrideComponent(AdditionalOptionsSectionComponent, { + remove: { + imports: [CustomFieldsComponent], + }, + add: { + imports: [MockCustomFieldsComponent], + }, + }) + .compileComponents(); fixture = TestBed.createComponent(AdditionalOptionsSectionComponent); component = fixture.componentInstance; diff --git a/libs/vault/src/cipher-form/components/additional-options/additional-options-section.component.ts b/libs/vault/src/cipher-form/components/additional-options/additional-options-section.component.ts index 9cd1c2ac5cd..6c061e1eeab 100644 --- a/libs/vault/src/cipher-form/components/additional-options/additional-options-section.component.ts +++ b/libs/vault/src/cipher-form/components/additional-options/additional-options-section.component.ts @@ -1,5 +1,5 @@ import { CommonModule } from "@angular/common"; -import { Component, OnInit } from "@angular/core"; +import { ChangeDetectorRef, Component, OnInit, ViewChild } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { FormBuilder, ReactiveFormsModule } from "@angular/forms"; import { shareReplay } from "rxjs"; @@ -10,6 +10,7 @@ import { CardComponent, CheckboxModule, FormFieldModule, + LinkModule, SectionComponent, SectionHeaderComponent, TypographyModule, @@ -17,12 +18,14 @@ import { import { PasswordRepromptService } from "../../../services/password-reprompt.service"; import { CipherFormContainer } from "../../cipher-form-container"; +import { CustomFieldsComponent } from "../custom-fields/custom-fields.component"; @Component({ selector: "vault-additional-options-section", templateUrl: "./additional-options-section.component.html", standalone: true, imports: [ + CommonModule, SectionComponent, SectionHeaderComponent, TypographyModule, @@ -32,9 +35,13 @@ import { CipherFormContainer } from "../../cipher-form-container"; ReactiveFormsModule, CheckboxModule, CommonModule, + CustomFieldsComponent, + LinkModule, ], }) export class AdditionalOptionsSectionComponent implements OnInit { + @ViewChild(CustomFieldsComponent) customFieldsComponent: CustomFieldsComponent; + additionalOptionsForm = this.formBuilder.group({ notes: [null as string], reprompt: [false], @@ -44,10 +51,17 @@ export class AdditionalOptionsSectionComponent implements OnInit { shareReplay({ refCount: false, bufferSize: 1 }), ); + /** When false when the add field button should be displayed in the Additional Options section */ + hasCustomFields = false; + + /** True when the form is in `partial-edit` mode */ + isPartialEdit = false; + constructor( private cipherFormContainer: CipherFormContainer, private formBuilder: FormBuilder, private passwordRepromptService: PasswordRepromptService, + private changeDetectorRef: ChangeDetectorRef, ) { this.cipherFormContainer.registerChildForm("additionalOptions", this.additionalOptionsForm); @@ -70,6 +84,22 @@ export class AdditionalOptionsSectionComponent implements OnInit { if (this.cipherFormContainer.config.mode === "partial-edit") { this.additionalOptionsForm.disable(); + this.isPartialEdit = true; } } + + /** Opens the add custom field dialog */ + addCustomField() { + this.customFieldsComponent.openAddEditCustomFieldDialog(); + } + + /** Update the local state when the number of fields changes */ + handleCustomFieldChange(numberOfCustomFields: number) { + this.hasCustomFields = numberOfCustomFields > 0; + + // The event that triggers `handleCustomFieldChange` can occur within + // the CustomFieldComponent `ngOnInit` lifecycle hook, so we need to + // manually trigger change detection to update the view. + this.changeDetectorRef.detectChanges(); + } } diff --git a/libs/vault/src/cipher-form/components/cipher-form.component.ts b/libs/vault/src/cipher-form/components/cipher-form.component.ts index 00226b25ea6..6f01f65be85 100644 --- a/libs/vault/src/cipher-form/components/cipher-form.component.ts +++ b/libs/vault/src/cipher-form/components/cipher-form.component.ts @@ -110,6 +110,7 @@ export class CipherFormComponent implements AfterViewInit, OnInit, OnChanges, Ci * @protected */ protected updatedCipherView: CipherView | null; + protected loading: boolean = true; CipherType = CipherType; diff --git a/libs/vault/src/cipher-form/components/custom-fields/add-edit-custom-field-dialog/add-edit-custom-field-dialog.component.html b/libs/vault/src/cipher-form/components/custom-fields/add-edit-custom-field-dialog/add-edit-custom-field-dialog.component.html new file mode 100644 index 00000000000..5f33d10b7d5 --- /dev/null +++ b/libs/vault/src/cipher-form/components/custom-fields/add-edit-custom-field-dialog/add-edit-custom-field-dialog.component.html @@ -0,0 +1,48 @@ + + + + {{ (variant === "add" ? "addField" : "editField") | i18n }} + +
+ + {{ "fieldType" | i18n }} + + + + + {{ getTypeHint() }} + + + + + {{ "fieldLabel" | i18n }} + + + {{ "linkedLabelHelpText" | i18n }} + + +
+
+ + + + +
+
+ diff --git a/libs/vault/src/cipher-form/components/custom-fields/add-edit-custom-field-dialog/add-edit-custom-field-dialog.component.spec.ts b/libs/vault/src/cipher-form/components/custom-fields/add-edit-custom-field-dialog/add-edit-custom-field-dialog.component.spec.ts new file mode 100644 index 00000000000..3ecb04cdc5c --- /dev/null +++ b/libs/vault/src/cipher-form/components/custom-fields/add-edit-custom-field-dialog/add-edit-custom-field-dialog.component.spec.ts @@ -0,0 +1,72 @@ +import { DIALOG_DATA } from "@angular/cdk/dialog"; +import { ComponentFixture, TestBed } from "@angular/core/testing"; + +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { FieldType } from "@bitwarden/common/vault/enums"; + +import { + AddEditCustomFieldDialogComponent, + AddEditCustomFieldDialogData, +} from "./add-edit-custom-field-dialog.component"; + +describe("AddEditCustomFieldDialogComponent", () => { + let component: AddEditCustomFieldDialogComponent; + let fixture: ComponentFixture; + const addField = jest.fn(); + const updateLabel = jest.fn(); + const removeField = jest.fn(); + + const dialogData = { + addField, + updateLabel, + removeField, + } as AddEditCustomFieldDialogData; + + beforeEach(async () => { + addField.mockClear(); + updateLabel.mockClear(); + removeField.mockClear(); + + await TestBed.configureTestingModule({ + imports: [AddEditCustomFieldDialogComponent], + providers: [ + { provide: I18nService, useValue: { t: (key: string) => key } }, + { provide: DIALOG_DATA, useValue: dialogData }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(AddEditCustomFieldDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges; + }); + + it("creates", () => { + expect(component).toBeTruthy(); + }); + + it("calls `addField` from DIALOG_DATA on with the type and label", () => { + component.customFieldForm.setValue({ type: FieldType.Text, label: "Test Label" }); + + component.submit(); + + expect(addField).toHaveBeenCalledWith(FieldType.Text, "Test Label"); + }); + + it("calls `updateLabel` from DIALOG_DATA with the new label", () => { + component.variant = "edit"; + dialogData.editLabelConfig = { index: 0, label: "Test Label" }; + component.customFieldForm.setValue({ type: FieldType.Text, label: "Test Label 2" }); + + component.submit(); + + expect(updateLabel).toHaveBeenCalledWith(0, "Test Label 2"); + }); + + it("calls `removeField` from DIALOG_DATA with the respective index", () => { + dialogData.editLabelConfig = { index: 2, label: "Test Label" }; + + component.removeField(); + + expect(removeField).toHaveBeenCalledWith(2); + }); +}); diff --git a/libs/vault/src/cipher-form/components/custom-fields/add-edit-custom-field-dialog/add-edit-custom-field-dialog.component.ts b/libs/vault/src/cipher-form/components/custom-fields/add-edit-custom-field-dialog/add-edit-custom-field-dialog.component.ts new file mode 100644 index 00000000000..f08d0ca40ed --- /dev/null +++ b/libs/vault/src/cipher-form/components/custom-fields/add-edit-custom-field-dialog/add-edit-custom-field-dialog.component.ts @@ -0,0 +1,120 @@ +import { DIALOG_DATA } from "@angular/cdk/dialog"; +import { CommonModule } from "@angular/common"; +import { Component, Inject } from "@angular/core"; +import { FormBuilder, ReactiveFormsModule, Validators } from "@angular/forms"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { FieldType } from "@bitwarden/common/vault/enums"; +import { + AsyncActionsModule, + ButtonModule, + DialogModule, + FormFieldModule, + IconButtonModule, + SelectModule, +} from "@bitwarden/components"; + +export type AddEditCustomFieldDialogData = { + addField: (type: FieldType, label: string) => void; + updateLabel: (index: number, label: string) => void; + removeField: (index: number) => void; + /** When provided, dialog will display edit label variants */ + editLabelConfig?: { index: number; label: string }; +}; + +@Component({ + standalone: true, + selector: "vault-add-edit-custom-field-dialog", + templateUrl: "./add-edit-custom-field-dialog.component.html", + imports: [ + CommonModule, + JslibModule, + DialogModule, + ButtonModule, + FormFieldModule, + SelectModule, + ReactiveFormsModule, + IconButtonModule, + AsyncActionsModule, + ], +}) +export class AddEditCustomFieldDialogComponent { + variant: "add" | "edit"; + + customFieldForm = this.formBuilder.group({ + type: FieldType.Text, + label: ["", Validators.required], + }); + + fieldTypeOptions = [ + { name: this.i18nService.t("cfTypeText"), value: FieldType.Text }, + { name: this.i18nService.t("cfTypeHidden"), value: FieldType.Hidden }, + { name: this.i18nService.t("cfTypeBoolean"), value: FieldType.Boolean }, + { name: this.i18nService.t("cfTypeLinked"), value: FieldType.Linked }, + ]; + + FieldType = FieldType; + + constructor( + @Inject(DIALOG_DATA) private data: AddEditCustomFieldDialogData, + private formBuilder: FormBuilder, + private i18nService: I18nService, + ) { + this.variant = data.editLabelConfig ? "edit" : "add"; + + if (this.variant === "edit") { + this.customFieldForm.controls.label.setValue(data.editLabelConfig.label); + this.customFieldForm.controls.type.disable(); + } + } + + getTypeHint(): string { + switch (this.customFieldForm.get("type")?.value) { + case FieldType.Text: + return this.i18nService.t("textHelpText"); + case FieldType.Hidden: + return this.i18nService.t("hiddenHelpText"); + case FieldType.Boolean: + return this.i18nService.t("checkBoxHelpText"); + case FieldType.Linked: + return this.i18nService.t("linkedHelpText"); + default: + return ""; + } + } + + /** Direct the form submission to the proper action */ + submit = () => { + if (this.variant === "add") { + this.addField(); + } else { + this.updateLabel(); + } + }; + + /** Invoke the `addField` callback with the custom field details */ + addField() { + if (this.customFieldForm.invalid) { + return; + } + + const { type, label } = this.customFieldForm.value; + this.data.addField(type, label); + } + + /** Invoke the `updateLabel` callback with the new label */ + updateLabel() { + if (this.customFieldForm.invalid) { + return; + } + + const { label } = this.customFieldForm.value; + this.data.updateLabel(this.data.editLabelConfig.index, label); + } + + /** Invoke the `removeField` callback */ + removeField() { + this.data.removeField(this.data.editLabelConfig.index); + } +} 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 new file mode 100644 index 00000000000..49362b9421f --- /dev/null +++ b/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.html @@ -0,0 +1,111 @@ + + +

{{ "customFields" | i18n }}

+
+
+ +
+ + + {{ field.value.name }} + + + + + + {{ field.value.name }} + + + + + + + + {{ field.value.name }} + + + + + {{ field.value.name }} + + + + + + + + +
+ + +
+
+
diff --git a/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.spec.ts b/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.spec.ts new file mode 100644 index 00000000000..7befcd59b0a --- /dev/null +++ b/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.spec.ts @@ -0,0 +1,373 @@ +import { LiveAnnouncer } from "@angular/cdk/a11y"; +import { DialogRef } from "@angular/cdk/dialog"; +import { CdkDragDrop } from "@angular/cdk/drag-drop"; +import { DebugElement } from "@angular/core"; +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { By } from "@angular/platform-browser"; + +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { CipherType, FieldType, LoginLinkedId } from "@bitwarden/common/vault/enums"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { FieldView } from "@bitwarden/common/vault/models/view/field.view"; +import { LoginView } from "@bitwarden/common/vault/models/view/login.view"; +import { DialogService } from "@bitwarden/components"; + +import { BitPasswordInputToggleDirective } from "../../../../../components/src/form-field/password-input-toggle.directive"; +import { CipherFormContainer } from "../../cipher-form-container"; + +import { CustomField, CustomFieldsComponent } from "./custom-fields.component"; + +const mockFieldViews = [ + { type: FieldType.Text, name: "text label", value: "text value" }, + { type: FieldType.Hidden, name: "hidden label", value: "hidden value" }, + { type: FieldType.Boolean, name: "boolean label", value: "true" }, + { type: FieldType.Linked, name: "linked label", value: null, linkedId: 1 }, +] as FieldView[]; + +let originalCipherView: CipherView | null = new CipherView(); +originalCipherView.type = CipherType.Login; +originalCipherView.login = new LoginView(); + +describe("CustomFieldsComponent", () => { + let component: CustomFieldsComponent; + let fixture: ComponentFixture; + let open: jest.Mock; + let announce: jest.Mock; + let patchCipher: jest.Mock; + + beforeEach(async () => { + open = jest.fn(); + announce = jest.fn().mockResolvedValue(null); + patchCipher = jest.fn(); + originalCipherView = new CipherView(); + originalCipherView.type = CipherType.Login; + originalCipherView.login = new LoginView(); + + await TestBed.configureTestingModule({ + imports: [CustomFieldsComponent], + providers: [ + { + provide: I18nService, + useValue: { t: (...keys: string[]) => keys.filter(Boolean).join(" ") }, + }, + { + provide: CipherFormContainer, + useValue: { patchCipher, originalCipherView, registerChildForm: jest.fn(), config: {} }, + }, + { + provide: LiveAnnouncer, + useValue: { announce }, + }, + ], + }) + .overrideProvider(DialogService, { + useValue: { + open, + }, + }) + .compileComponents(); + + fixture = TestBed.createComponent(CustomFieldsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + describe("initializing", () => { + it("populates linkedFieldOptions", () => { + originalCipherView.login.linkedFieldOptions = new Map([ + [1, { i18nKey: "one-i18", propertyKey: "one" }], + [2, { i18nKey: "two-i18", propertyKey: "two" }], + ]); + + component.ngOnInit(); + + expect(component.linkedFieldOptions).toEqual([ + { value: 1, name: "one-i18" }, + { value: 2, name: "two-i18" }, + ]); + }); + + it("populates customFieldsForm", () => { + originalCipherView.fields = mockFieldViews; + + component.ngOnInit(); + + expect(component.fields.value).toEqual([ + { + linkedId: null, + name: "text label", + type: FieldType.Text, + value: "text value", + newField: false, + }, + { + linkedId: null, + name: "hidden label", + type: FieldType.Hidden, + value: "hidden value", + newField: false, + }, + { + linkedId: null, + name: "boolean label", + type: FieldType.Boolean, + value: true, + newField: false, + }, + { linkedId: 1, name: "linked label", type: FieldType.Linked, value: null, newField: false }, + ]); + }); + + it("forbids a user to view hidden fields when the cipher `viewPassword` is false", () => { + originalCipherView.viewPassword = false; + originalCipherView.fields = mockFieldViews; + + component.ngOnInit(); + + fixture.detectChanges(); + + const button = fixture.debugElement.query(By.directive(BitPasswordInputToggleDirective)); + + expect(button.nativeElement.disabled).toBe(true); + }); + }); + + describe("adding new field", () => { + let close: jest.Mock; + + beforeEach(() => { + close = jest.fn(); + component.dialogRef = { close } as unknown as DialogRef; + }); + + it("closes the add dialog", () => { + component.addField(FieldType.Text, "test label"); + + expect(close).toHaveBeenCalled(); + }); + + it("adds a unselected boolean field", () => { + component.addField(FieldType.Boolean, "bool label"); + + expect(component.fields.value).toEqual([ + { + linkedId: null, + name: "bool label", + type: FieldType.Boolean, + value: false, + newField: true, + }, + ]); + }); + + it("auto-selects the first linked field option", () => { + component.linkedFieldOptions = [ + { value: LoginLinkedId.Password, name: "one" }, + { value: LoginLinkedId.Username, name: "two" }, + ]; + + component.addField(FieldType.Linked, "linked label"); + + expect(component.fields.value).toEqual([ + { + linkedId: LoginLinkedId.Password, + name: "linked label", + type: FieldType.Linked, + value: null, + newField: true, + }, + ]); + }); + + it("adds text field", () => { + component.addField(FieldType.Text, "text label"); + + expect(component.fields.value).toEqual([ + { linkedId: null, name: "text label", type: FieldType.Text, value: null, newField: true }, + ]); + }); + + it("adds hidden field", () => { + component.addField(FieldType.Hidden, "hidden label"); + + expect(component.fields.value).toEqual([ + { + linkedId: null, + name: "hidden label", + type: FieldType.Hidden, + value: null, + newField: true, + }, + ]); + }); + + it("announces the new input field", () => { + component.addField(FieldType.Text, "text label 2"); + + fixture.detectChanges(); + + expect(announce).toHaveBeenCalledWith("fieldAdded text label 2", "polite"); + }); + + it("allows a user to view hidden fields when the cipher `viewPassword` is false", () => { + originalCipherView.viewPassword = false; + component.addField(FieldType.Hidden, "Hidden label"); + + fixture.detectChanges(); + + const button = fixture.debugElement.query(By.directive(BitPasswordInputToggleDirective)); + + expect(button.nativeElement.disabled).toBe(false); + }); + }); + + describe("updating a field", () => { + beforeEach(() => { + originalCipherView.fields = [mockFieldViews[0]]; + + component.ngOnInit(); + }); + + it("updates the value", () => { + component.fields.at(0).patchValue({ value: "new text value" }); + + const fieldView = new FieldView(); + fieldView.name = "text label"; + fieldView.value = "new text value"; + fieldView.type = FieldType.Text; + + expect(patchCipher).toHaveBeenCalledWith({ fields: [fieldView] }); + }); + + it("updates the label", () => { + component.updateLabel(0, "new text label"); + + const fieldView = new FieldView(); + fieldView.name = "new text label"; + fieldView.value = "text value"; + fieldView.type = FieldType.Text; + + expect(patchCipher).toHaveBeenCalledWith({ fields: [fieldView] }); + }); + }); + + describe("removing field", () => { + beforeEach(() => { + originalCipherView.fields = [mockFieldViews[0]]; + + component.ngOnInit(); + }); + + it("removes the field", () => { + component.removeField(0); + + expect(patchCipher).toHaveBeenCalledWith({ fields: [] }); + }); + }); + + describe("reordering fields", () => { + let toggleItems: DebugElement[]; + + beforeEach(() => { + originalCipherView.fields = mockFieldViews; + + component.ngOnInit(); + + fixture.detectChanges(); + + toggleItems = fixture.debugElement.queryAll( + By.css('button[data-testid="reorder-toggle-button"]'), + ); + }); + + it("reorders the fields when dropped", () => { + expect(component.fields.value.map((f: CustomField) => f.name)).toEqual([ + "text label", + "hidden label", + "boolean label", + "linked label", + ]); + + // Move second field to first + component.drop({ previousIndex: 0, currentIndex: 1 } as CdkDragDrop); + + const latestCallParams = patchCipher.mock.lastCall[0]; + + expect(latestCallParams.fields.map((f: FieldView) => f.name)).toEqual([ + "hidden label", + "text label", + "boolean label", + "linked label", + ]); + }); + + it("moves an item down in order via keyboard", () => { + // Move 3rd item (boolean label) down to 4th + toggleItems[2].triggerEventHandler("keydown", { + key: "ArrowDown", + preventDefault: jest.fn(), + }); + + const latestCallParams = patchCipher.mock.lastCall[0]; + + expect(latestCallParams.fields.map((f: FieldView) => f.name)).toEqual([ + "text label", + "hidden label", + "linked label", + "boolean label", + ]); + }); + + it("moves an item up in order via keyboard", () => { + // Move 2nd item (hidden label) up to 1st + toggleItems[1].triggerEventHandler("keydown", { key: "ArrowUp", preventDefault: jest.fn() }); + + const latestCallParams = patchCipher.mock.lastCall[0]; + + expect(latestCallParams.fields.map((f: FieldView) => f.name)).toEqual([ + "hidden label", + "text label", + "boolean label", + "linked label", + ]); + }); + + it("does not move the first item up", () => { + patchCipher.mockClear(); + + toggleItems[0].triggerEventHandler("keydown", { key: "ArrowUp", preventDefault: jest.fn() }); + + expect(patchCipher).not.toHaveBeenCalled(); + }); + + it("does not move the last item down", () => { + patchCipher.mockClear(); + + toggleItems[toggleItems.length - 1].triggerEventHandler("keydown", { + key: "ArrowDown", + preventDefault: jest.fn(), + }); + + expect(patchCipher).not.toHaveBeenCalled(); + }); + + it("announces the reorder up", () => { + // Move 2nd item up to 1st + toggleItems[1].triggerEventHandler("keydown", { key: "ArrowUp", preventDefault: jest.fn() }); + + // "reorder hidden label to position 1 of 4" + expect(announce).toHaveBeenCalledWith("reorderFieldUp hidden label 1 4", "assertive"); + }); + + it("announces the reorder down", () => { + // Move 3rd item down to 4th + toggleItems[2].triggerEventHandler("keydown", { + key: "ArrowDown", + preventDefault: jest.fn(), + }); + + // "reorder boolean label to position 4 of 4" + expect(announce).toHaveBeenCalledWith("reorderFieldDown boolean label 4 4", "assertive"); + }); + }); +}); diff --git a/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.ts b/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.ts new file mode 100644 index 00000000000..0233e1c1b17 --- /dev/null +++ b/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.ts @@ -0,0 +1,334 @@ +import { LiveAnnouncer } from "@angular/cdk/a11y"; +import { DialogRef } from "@angular/cdk/dialog"; +import { CdkDragDrop, DragDropModule, moveItemInArray } from "@angular/cdk/drag-drop"; +import { CommonModule } from "@angular/common"; +import { + AfterViewInit, + Component, + DestroyRef, + ElementRef, + EventEmitter, + OnInit, + Output, + QueryList, + ViewChildren, + inject, +} from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { FormArray, FormBuilder, FormsModule, ReactiveFormsModule } from "@angular/forms"; +import { Subject, switchMap, take } from "rxjs"; + +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 { FieldType, LinkedIdType } from "@bitwarden/common/vault/enums"; +import { FieldView } from "@bitwarden/common/vault/models/view/field.view"; +import { + DialogService, + SectionComponent, + SectionHeaderComponent, + FormFieldModule, + TypographyModule, + CardComponent, + IconButtonModule, + CheckboxModule, + SelectModule, + LinkModule, +} from "@bitwarden/components"; + +import { CipherFormContainer } from "../../cipher-form-container"; + +import { + AddEditCustomFieldDialogComponent, + AddEditCustomFieldDialogData, +} from "./add-edit-custom-field-dialog/add-edit-custom-field-dialog.component"; + +/** Attributes associated with each individual FormGroup within the FormArray */ +export type CustomField = { + type: FieldType; + name: string; + value: string | boolean | null; + linkedId: LinkedIdType; + /** + * `newField` is set to true when the custom field is created. + * + * This is applicable when the user is adding a new field but + * the `viewPassword` property on the cipher is false. The + * user will still need the ability to set the value of the field + * they just created. + * + * See {@link CustomFieldsComponent.canViewPasswords} for implementation. + */ + newField: boolean; +}; + +@Component({ + standalone: true, + selector: "vault-custom-fields", + templateUrl: "./custom-fields.component.html", + imports: [ + JslibModule, + CommonModule, + FormsModule, + FormFieldModule, + ReactiveFormsModule, + SectionComponent, + SectionHeaderComponent, + TypographyModule, + CardComponent, + IconButtonModule, + CheckboxModule, + SelectModule, + DragDropModule, + LinkModule, + ], +}) +export class CustomFieldsComponent implements OnInit, AfterViewInit { + @Output() numberOfFieldsChange = new EventEmitter(); + + @ViewChildren("customFieldRow") customFieldRows: QueryList>; + + customFieldsForm = this.formBuilder.group({ + fields: new FormArray([]), + }); + + /** Reference to the add field dialog */ + dialogRef: DialogRef; + + /** Options for Linked Fields */ + linkedFieldOptions: { name: string; value: LinkedIdType }[] = []; + + /** True when edit/reorder toggles should be hidden based on partial-edit */ + isPartialEdit: boolean; + + /** True when there are custom fields available */ + hasCustomFields = false; + + /** Emits when a new custom field should be focused */ + private focusOnNewInput$ = new Subject(); + + destroyed$: DestroyRef; + FieldType = FieldType; + + constructor( + private dialogService: DialogService, + private cipherFormContainer: CipherFormContainer, + private formBuilder: FormBuilder, + private i18nService: I18nService, + private liveAnnouncer: LiveAnnouncer, + ) { + this.destroyed$ = inject(DestroyRef); + this.cipherFormContainer.registerChildForm("customFields", this.customFieldsForm); + + this.customFieldsForm.valueChanges.pipe(takeUntilDestroyed()).subscribe((values) => { + this.updateCipher(values.fields); + }); + } + + /** Fields form array, referenced via a getter to avoid type-casting in multiple places */ + get fields(): FormArray { + return this.customFieldsForm.controls.fields as FormArray; + } + + ngOnInit() { + // Populate options for linked custom fields + this.linkedFieldOptions = Array.from( + this.cipherFormContainer.originalCipherView?.linkedFieldOptions?.entries() ?? [], + ) + .map(([id, linkedFieldOption]) => ({ + name: this.i18nService.t(linkedFieldOption.i18nKey), + value: id, + })) + .sort(Utils.getSortFunction(this.i18nService, "name")); + + // Populate the form with the existing fields + this.cipherFormContainer.originalCipherView?.fields?.forEach((field) => { + let value: string | boolean = field.value; + + if (field.type === FieldType.Boolean) { + value = field.value === "true" ? true : false; + } + + this.fields.push( + this.formBuilder.group({ + type: field.type, + name: field.name, + value: value, + linkedId: field.linkedId, + newField: false, + }), + ); + }); + + // Disable the form if in partial-edit mode + // Must happen after the initial fields are populated + if (this.cipherFormContainer.config.mode === "partial-edit") { + this.isPartialEdit = true; + this.customFieldsForm.disable(); + } + } + + ngAfterViewInit(): void { + // Focus on the new input field when it is added + // This is done after the view is initialized to ensure the input is rendered + this.focusOnNewInput$ + .pipe( + takeUntilDestroyed(this.destroyed$), + // QueryList changes are emitted after the view is updated + switchMap(() => this.customFieldRows.changes.pipe(take(1))), + ) + .subscribe(() => { + const input = + this.customFieldRows.last.nativeElement.querySelector("input"); + const label = document.querySelector(`label[for="${input.id}"]`).textContent.trim(); + + // Focus the input after the announcement element is added to the DOM, + // this should stop the announcement from being cut off by the "focus" event. + void this.liveAnnouncer + .announce(this.i18nService.t("fieldAdded", label), "polite") + .then(() => { + input.focus(); + }); + }); + } + + /** Opens the add/edit custom field dialog */ + openAddEditCustomFieldDialog(editLabelConfig?: AddEditCustomFieldDialogData["editLabelConfig"]) { + this.dialogRef = this.dialogService.open( + AddEditCustomFieldDialogComponent, + { + data: { + addField: this.addField.bind(this), + updateLabel: this.updateLabel.bind(this), + removeField: this.removeField.bind(this), + editLabelConfig, + }, + }, + ); + } + + /** Returns true when the user has permission to view passwords for the individual cipher */ + canViewPasswords(index: number) { + if (this.cipherFormContainer.originalCipherView === null) { + return true; + } + + return ( + this.cipherFormContainer.originalCipherView.viewPassword || + this.fields.at(index).value.newField + ); + } + + /** Updates label for an individual field */ + updateLabel(index: number, label: string) { + this.fields.at(index).patchValue({ name: label }); + this.dialogRef?.close(); + } + + /** Removes an individual field at a specific index */ + removeField(index: number) { + this.fields.removeAt(index); + this.dialogRef?.close(); + } + + /** Adds a new field to the form */ + addField(type: FieldType, label: string) { + this.dialogRef?.close(); + + let value = null; + let linkedId = null; + + if (type === FieldType.Boolean) { + // Default to false for boolean fields + value = false; + } + + if (type === FieldType.Linked && this.linkedFieldOptions.length > 0) { + // Default to the first linked field option + linkedId = this.linkedFieldOptions[0].value; + } + + this.fields.push( + this.formBuilder.group({ + type, + name: label, + value, + linkedId, + newField: true, + }), + ); + + // Trigger focus on the new input field + this.focusOnNewInput$.next(); + } + + /** Reorder the controls to match the new order after a "drop" event */ + drop(event: CdkDragDrop) { + // Alter the order of the fields array in place + moveItemInArray(this.fields.controls, event.previousIndex, event.currentIndex); + + this.updateCipher(this.fields.controls.map((control) => control.value)); + } + + /** Move a custom field up or down in the list order */ + async handleKeyDown(event: KeyboardEvent, label: string, index: number) { + if (event.key === "ArrowUp" && index !== 0) { + event.preventDefault(); + + const currentIndex = index - 1; + this.drop({ previousIndex: index, currentIndex } as CdkDragDrop); + await this.liveAnnouncer.announce( + this.i18nService.t("reorderFieldUp", label, currentIndex + 1, this.fields.length), + "assertive", + ); + + // Refocus the button after the reorder + // Angular re-renders the list when moving an item up which causes the focus to be lost + // Wait for the next tick to ensure the button is rendered before focusing + setTimeout(() => { + (event.target as HTMLButtonElement).focus(); + }); + } + + if (event.key === "ArrowDown" && index !== this.fields.length - 1) { + event.preventDefault(); + + const currentIndex = index + 1; + this.drop({ previousIndex: index, currentIndex } as CdkDragDrop); + await this.liveAnnouncer.announce( + this.i18nService.t("reorderFieldDown", label, currentIndex + 1, this.fields.length), + "assertive", + ); + } + } + + /** Create `FieldView` from the form objects and update the cipher */ + private updateCipher(fields: CustomField[]) { + const newFields = fields.map((field: CustomField) => { + let value: string; + + if (typeof field.value === "number") { + value = `${field.value}`; + } else if (typeof field.value === "boolean") { + value = field.value ? "true" : "false"; + } else { + value = field.value; + } + + const fieldView = new FieldView(); + fieldView.type = field.type; + fieldView.name = field.name; + fieldView.value = value; + fieldView.linkedId = field.linkedId; + return fieldView; + }); + + this.hasCustomFields = newFields.length > 0; + + this.numberOfFieldsChange.emit(newFields.length); + + this.cipherFormContainer.patchCipher({ + fields: newFields, + }); + } +} From e27d698d4b4564944fda9865f5be9a62ba8f2f26 Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Wed, 17 Jul 2024 10:12:40 -0400 Subject: [PATCH 49/57] [AC-2860] Revise unassigned and purchased seat warning for CB (#10077) * Rework create-client-dialog seat warning * Rework manage-client-subscription-dialog seat warning * Fix create client purchased seats label * Fix manage client subscription purchased seats label logic * Another manage subscription purchased seats fix --- apps/web/src/locales/en/messages.json | 3 ++ .../create-client-dialog.component.html | 9 ++++-- .../clients/create-client-dialog.component.ts | 19 +++++++----- ...-client-subscription-dialog.component.html | 14 ++++++--- ...ge-client-subscription-dialog.component.ts | 31 +++++++++++++++++-- 5 files changed, 59 insertions(+), 17 deletions(-) diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 8b8c2656531..73396c39c16 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -8564,5 +8564,8 @@ "example": "Organization name" } } + }, + "purchasedSeatsRemoved": { + "message": "purchased seats removed" } } diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/create-client-dialog.component.html b/bitwarden_license/bit-web/src/app/billing/providers/clients/create-client-dialog.component.html index 662cd8a69fa..66ac422441a 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/clients/create-client-dialog.component.html +++ b/bitwarden_license/bit-web/src/app/billing/providers/clients/create-client-dialog.component.html @@ -58,13 +58,16 @@ {{ unassignedSeats }} {{ "unassignedSeatsDescription" | i18n | lowercase }} - {{ additionalSeatsPurchased }} {{ "purchaseSeatDescription" | i18n | lowercase }} diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/create-client-dialog.component.ts b/bitwarden_license/bit-web/src/app/billing/providers/clients/create-client-dialog.component.ts index 987b7cc6982..c0ee21d2ab3 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/clients/create-client-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/billing/providers/clients/create-client-dialog.component.ts @@ -162,18 +162,16 @@ export class CreateClientDialogComponent implements OnInit { this.dialogRef.close(this.ResultType.Submitted); }; - protected get openSeats(): number { + protected get unassignedSeats(): number { const selectedProviderPlan = this.getSelectedProviderPlan(); if (selectedProviderPlan === null) { return 0; } - return selectedProviderPlan.seatMinimum - selectedProviderPlan.assignedSeats; - } + const openSeats = selectedProviderPlan.seatMinimum - selectedProviderPlan.assignedSeats; - protected get unassignedSeats(): number { - const unassignedSeats = this.openSeats - this.formGroup.value.seats; + const unassignedSeats = openSeats - this.formGroup.value.seats; return unassignedSeats > 0 ? unassignedSeats : 0; } @@ -185,11 +183,16 @@ export class CreateClientDialogComponent implements OnInit { return 0; } - const selectedSeats = this.formGroup.value.seats ?? 0; + if (selectedProviderPlan.purchasedSeats > 0) { + return this.formGroup.value.seats; + } - const purchased = selectedSeats - this.openSeats; + const additionalSeatsPurchased = + this.formGroup.value.seats + + selectedProviderPlan.assignedSeats - + selectedProviderPlan.seatMinimum; - return purchased > 0 ? purchased : 0; + return additionalSeatsPurchased > 0 ? additionalSeatsPurchased : 0; } private getSelectedProviderPlan(): ProviderPlanResponse { diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-client-subscription-dialog.component.html b/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-client-subscription-dialog.component.html index 0d2d22eadd9..2c911b2cb15 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-client-subscription-dialog.component.html +++ b/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-client-subscription-dialog.component.html @@ -16,21 +16,27 @@ formControlName="assignedSeats" [min]="dialogParams.organization.occupiedSeats" /> - +
{{ unassignedSeats }} {{ "unassignedSeatsDescription" | i18n | lowercase }} - {{ additionalSeatsPurchased }} {{ "purchaseSeatDescription" | i18n | lowercase }} + {{ purchasedSeatsRemoved }} {{ "purchasedSeatsRemoved" | i18n | lowercase }}
+
diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-client-subscription-dialog.component.ts b/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-client-subscription-dialog.component.ts index e97e4ea9596..f92223d1b54 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-client-subscription-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-client-subscription-dialog.component.ts @@ -36,7 +36,10 @@ export const openManageClientSubscriptionDialog = ( export class ManageClientSubscriptionDialogComponent implements OnInit { protected loading = true; protected providerPlan: ProviderPlanResponse; + protected assignedSeats: number; protected openSeats: number; + protected purchasedSeats: number; + protected seatMinimum: number; protected readonly ResultType = ManageClientSubscriptionDialogResultType; protected formGroup = new FormGroup({ @@ -63,7 +66,10 @@ export class ManageClientSubscriptionDialogComponent implements OnInit { (plan) => plan.planName === this.dialogParams.organization.plan, ); + this.assignedSeats = this.providerPlan.assignedSeats; this.openSeats = this.providerPlan.seatMinimum - this.providerPlan.assignedSeats; + this.purchasedSeats = this.providerPlan.purchasedSeats; + this.seatMinimum = this.providerPlan.seatMinimum; this.formGroup.controls.assignedSeats.addValidators( this.isServiceUserWithPurchasedSeats @@ -165,9 +171,22 @@ export class ManageClientSubscriptionDialogComponent implements OnInit { const seatDifference = this.formGroup.value.assignedSeats - this.dialogParams.organization.seats; - const purchasedSeats = seatDifference - this.openSeats; + if (this.purchasedSeats > 0) { + return seatDifference; + } - return purchasedSeats > 0 ? purchasedSeats : 0; + return seatDifference - this.openSeats; + } + + get purchasedSeatsRemoved(): number { + const seatDifference = + this.dialogParams.organization.seats - this.formGroup.value.assignedSeats; + + if (this.purchasedSeats >= seatDifference) { + return seatDifference; + } + + return this.purchasedSeats; } get isProviderAdmin(): boolean { @@ -177,4 +196,12 @@ export class ManageClientSubscriptionDialogComponent implements OnInit { get isServiceUserWithPurchasedSeats(): boolean { return !this.isProviderAdmin && this.providerPlan && this.providerPlan.purchasedSeats > 0; } + + get purchasingSeats(): boolean { + return this.additionalSeatsPurchased > 0; + } + + get sellingSeats(): boolean { + return this.purchasedSeats > 0 && this.additionalSeatsPurchased < 0; + } } From fd93c76c0dffc433a4d8ebae85ba786599c87e93 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Wed, 17 Jul 2024 16:31:35 +0200 Subject: [PATCH 50/57] Fix key rotation being broken due to org ciphers being included (#10140) --- libs/common/src/vault/services/cipher.service.spec.ts | 2 ++ libs/common/src/vault/services/cipher.service.ts | 9 +++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/libs/common/src/vault/services/cipher.service.spec.ts b/libs/common/src/vault/services/cipher.service.spec.ts index ba85f51c38b..c84fd066b27 100644 --- a/libs/common/src/vault/services/cipher.service.spec.ts +++ b/libs/common/src/vault/services/cipher.service.spec.ts @@ -352,8 +352,10 @@ describe("Cipher Service", () => { const cipher1 = new CipherView(cipherObj); cipher1.id = "Cipher 1"; + cipher1.organizationId = null; const cipher2 = new CipherView(cipherObj); cipher2.id = "Cipher 2"; + cipher2.organizationId = null; decryptedCiphers = new BehaviorSubject({ Cipher1: cipher1, diff --git a/libs/common/src/vault/services/cipher.service.ts b/libs/common/src/vault/services/cipher.service.ts index d2d28c2d812..1d06ae1dd0b 100644 --- a/libs/common/src/vault/services/cipher.service.ts +++ b/libs/common/src/vault/services/cipher.service.ts @@ -1184,11 +1184,16 @@ export class CipherService implements CipherServiceAbstraction { let encryptedCiphers: CipherWithIdRequest[] = []; const ciphers = await this.getAllDecrypted(); - if (!ciphers || ciphers.length === 0) { + if (!ciphers) { + return encryptedCiphers; + } + + const userCiphers = ciphers.filter((c) => c.organizationId == null); + if (userCiphers.length === 0) { return encryptedCiphers; } encryptedCiphers = await Promise.all( - ciphers.map(async (cipher) => { + userCiphers.map(async (cipher) => { const encryptedCipher = await this.encrypt(cipher, newUserKey, originalUserKey); return new CipherWithIdRequest(encryptedCipher); }), From 3706eb995aa310e602f877d78f0dd67b1b377703 Mon Sep 17 00:00:00 2001 From: Todd Martin <106564991+trmartin4@users.noreply.github.com> Date: Wed, 17 Jul 2024 15:48:55 -0400 Subject: [PATCH 51/57] Remove labeling of PRs with needs-qa (#9880) --- .../workflows/label-issue-pull-request.yml | 24 ------------------- 1 file changed, 24 deletions(-) delete mode 100644 .github/workflows/label-issue-pull-request.yml diff --git a/.github/workflows/label-issue-pull-request.yml b/.github/workflows/label-issue-pull-request.yml deleted file mode 100644 index e52bba36d63..00000000000 --- a/.github/workflows/label-issue-pull-request.yml +++ /dev/null @@ -1,24 +0,0 @@ -# Runs creation of Pull Requests -# If the PR destination branch is main, add a needs-qa label unless created by renovate[bot] ---- -name: Label Issue Pull Request - -on: - pull_request: - types: - - opened # Check when PR is opened - paths-ignore: - - .github/workflows/** # We don't need QA on workflow changes - branches: - - 'main' # We only want to check when PRs target main - -jobs: - add-needs-qa-label: - runs-on: ubuntu-latest - if: ${{ github.actor != 'renovate[bot]' }} - steps: - - name: Add label to pull request - uses: andymckay/labeler@e6c4322d0397f3240f0e7e30a33b5c5df2d39e90 # 1.0.4 - if: ${{ !github.event.pull_request.head.repo.fork }} - with: - add-labels: "needs-qa" From 9b50e5c496aa51a3aae747a18261d01fd04363a6 Mon Sep 17 00:00:00 2001 From: Addison Beck Date: Thu, 18 Jul 2024 05:17:53 -0400 Subject: [PATCH 52/57] Disable a button during page load (#10148) The people screen has a button that can be clicked to open a modal for inviting new users to an organization. This modal depends on data from the people list for conditional logic, like whether or not the seat count cor the organization has been reached. If the modal is opened before the people list loads these conditions can not process correctly, causing bugs. This commit disabled the button until the people list loads to prevent this kind of behavior. --- .../admin-console/organizations/members/members.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/app/admin-console/organizations/members/members.component.html b/apps/web/src/app/admin-console/organizations/members/members.component.html index f5b824face6..ae80130e03b 100644 --- a/apps/web/src/app/admin-console/organizations/members/members.component.html +++ b/apps/web/src/app/admin-console/organizations/members/members.component.html @@ -5,7 +5,7 @@ [placeholder]="'searchMembers' | i18n" > - From 84b719d797291c75db959f29f587a0e42479caf8 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Thu, 18 Jul 2024 14:56:22 +0200 Subject: [PATCH 53/57] [PM-4154] Introduce Bulk Encrypt Service for Faster Unlock Times (#6465) * Implement multi-worker encryption service * Fix feature flag being flipped and check for empty input earlier * Add tests * Small cleanup * Remove restricted import * Rename feature flag * Refactor to BulkEncryptService * Rename feature flag * Fix cipher service spec * Implement browser bulk encryption service * Un-deprecate browserbulkencryptservice * Load browser bulk encrypt service on feature flag asynchronously * Fix bulk encryption service factories * Deprecate BrowserMultithreadEncryptServiceImplementation * Copy tests for browser-bulk-encrypt-service-implementation from browser-multithread-encrypt-service-implementation * Make sure desktop uses non-bulk fallback during feature rollout * Rename FallbackBulkEncryptService and fix service dependency issue * Disable bulk encrypt service on mv3 * Change condition order to avoid expensive api call * Set default hardware concurrency to 1 if not available * Make getdecrypteditemfromworker private * Fix cli build * Add check for key being null --- .../browser/src/background/main.background.ts | 14 +- apps/cli/src/service-container.ts | 2 + .../services/emergency-access.service.spec.ts | 7 + .../services/emergency-access.service.ts | 21 ++- .../src/services/jslib-services.module.ts | 10 ++ libs/common/src/enums/feature-flag.enum.ts | 2 + .../abstractions/bulk-encrypt.service.ts | 10 ++ .../platform/abstractions/encrypt.service.ts | 5 + .../bulk-encrypt.service.implementation.ts | 164 ++++++++++++++++++ .../encrypt.service.implementation.ts | 3 + .../fallback-bulk-encrypt.service.ts | 33 ++++ ...tithread-encrypt.service.implementation.ts | 3 + .../src/vault/services/cipher.service.spec.ts | 4 + .../src/vault/services/cipher.service.ts | 30 +++- 14 files changed, 296 insertions(+), 12 deletions(-) create mode 100644 libs/common/src/platform/abstractions/bulk-encrypt.service.ts create mode 100644 libs/common/src/platform/services/cryptography/bulk-encrypt.service.implementation.ts create mode 100644 libs/common/src/platform/services/cryptography/fallback-bulk-encrypt.service.ts diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 9aac8464ab4..c271dd29db3 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -112,7 +112,9 @@ import { ConfigApiService } from "@bitwarden/common/platform/services/config/con import { DefaultConfigService } from "@bitwarden/common/platform/services/config/default-config.service"; import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service"; import { ContainerService } from "@bitwarden/common/platform/services/container.service"; +import { BulkEncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/bulk-encrypt.service.implementation"; import { EncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/encrypt.service.implementation"; +import { FallbackBulkEncryptService } from "@bitwarden/common/platform/services/cryptography/fallback-bulk-encrypt.service"; import { MultithreadEncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/multithread-encrypt.service.implementation"; import { Fido2AuthenticatorService } from "@bitwarden/common/platform/services/fido2/fido2-authenticator.service"; import { Fido2ClientService } from "@bitwarden/common/platform/services/fido2/fido2-client.service"; @@ -247,7 +249,6 @@ import CommandsBackground from "./commands.background"; import IdleBackground from "./idle.background"; import { NativeMessagingBackground } from "./nativeMessaging.background"; import RuntimeBackground from "./runtime.background"; - export default class MainBackground { messagingService: MessageSender; storageService: BrowserLocalStorageService; @@ -306,6 +307,7 @@ export default class MainBackground { vaultFilterService: VaultFilterService; usernameGenerationService: UsernameGenerationServiceAbstraction; encryptService: EncryptService; + bulkEncryptService: FallbackBulkEncryptService; folderApiService: FolderApiServiceAbstraction; policyApiService: PolicyApiServiceAbstraction; sendApiService: SendApiServiceAbstraction; @@ -744,6 +746,7 @@ export default class MainBackground { this.stateService, this.autofillSettingsService, this.encryptService, + this.bulkEncryptService, this.cipherFileUploadService, this.configService, this.stateProvider, @@ -1227,6 +1230,15 @@ export default class MainBackground { this.webRequestBackground?.startListening(); this.syncServiceListener?.listener$().subscribe(); + if ( + BrowserApi.isManifestVersion(2) && + (await this.configService.getFeatureFlag(FeatureFlag.PM4154_BulkEncryptionService)) + ) { + await this.bulkEncryptService.setFeatureFlagEncryptService( + new BulkEncryptServiceImplementation(this.cryptoFunctionService, this.logService), + ); + } + return new Promise((resolve) => { setTimeout(async () => { await this.refreshBadge(); diff --git a/apps/cli/src/service-container.ts b/apps/cli/src/service-container.ts index 3d53013ef0c..cfe310318fb 100644 --- a/apps/cli/src/service-container.ts +++ b/apps/cli/src/service-container.ts @@ -75,6 +75,7 @@ import { DefaultConfigService } from "@bitwarden/common/platform/services/config import { ContainerService } from "@bitwarden/common/platform/services/container.service"; import { CryptoService } from "@bitwarden/common/platform/services/crypto.service"; import { EncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/encrypt.service.implementation"; +import { FallbackBulkEncryptService } from "@bitwarden/common/platform/services/cryptography/fallback-bulk-encrypt.service"; import { DefaultEnvironmentService } from "@bitwarden/common/platform/services/default-environment.service"; import { FileUploadService } from "@bitwarden/common/platform/services/file-upload/file-upload.service"; import { KeyGenerationService } from "@bitwarden/common/platform/services/key-generation.service"; @@ -605,6 +606,7 @@ export class ServiceContainer { this.stateService, this.autofillSettingsService, this.encryptService, + new FallbackBulkEncryptService(this.encryptService), this.cipherFileUploadService, this.configService, this.stateProvider, diff --git a/apps/web/src/app/auth/emergency-access/services/emergency-access.service.spec.ts b/apps/web/src/app/auth/emergency-access/services/emergency-access.service.spec.ts index 7906731d81e..6f68821943b 100644 --- a/apps/web/src/app/auth/emergency-access/services/emergency-access.service.spec.ts +++ b/apps/web/src/app/auth/emergency-access/services/emergency-access.service.spec.ts @@ -4,6 +4,8 @@ import mock from "jest-mock-extended/lib/Mock"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ListResponse } from "@bitwarden/common/models/response/list.response"; import { UserKeyResponse } from "@bitwarden/common/models/response/user-key.response"; +import { BulkEncryptService } from "@bitwarden/common/platform/abstractions/bulk-encrypt.service"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -31,15 +33,18 @@ describe("EmergencyAccessService", () => { let apiService: MockProxy; let cryptoService: MockProxy; let encryptService: MockProxy; + let bulkEncryptService: MockProxy; let cipherService: MockProxy; let logService: MockProxy; let emergencyAccessService: EmergencyAccessService; + let configService: ConfigService; beforeAll(() => { emergencyAccessApiService = mock(); apiService = mock(); cryptoService = mock(); encryptService = mock(); + bulkEncryptService = mock(); cipherService = mock(); logService = mock(); @@ -48,8 +53,10 @@ describe("EmergencyAccessService", () => { apiService, cryptoService, encryptService, + bulkEncryptService, cipherService, logService, + configService, ); }); diff --git a/apps/web/src/app/auth/emergency-access/services/emergency-access.service.ts b/apps/web/src/app/auth/emergency-access/services/emergency-access.service.ts index 362b1dec3cc..5b9d73c75e5 100644 --- a/apps/web/src/app/auth/emergency-access/services/emergency-access.service.ts +++ b/apps/web/src/app/auth/emergency-access/services/emergency-access.service.ts @@ -9,6 +9,9 @@ import { KdfConfig, PBKDF2KdfConfig, } from "@bitwarden/common/auth/models/domain/kdf-config"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { BulkEncryptService } from "@bitwarden/common/platform/abstractions/bulk-encrypt.service"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -45,8 +48,10 @@ export class EmergencyAccessService private apiService: ApiService, private cryptoService: CryptoService, private encryptService: EncryptService, + private bulkEncryptService: BulkEncryptService, private cipherService: CipherService, private logService: LogService, + private configService: ConfigService, ) {} /** @@ -225,10 +230,18 @@ export class EmergencyAccessService ); const grantorUserKey = new SymmetricCryptoKey(grantorKeyBuffer) as UserKey; - const ciphers = await this.encryptService.decryptItems( - response.ciphers.map((c) => new Cipher(c)), - grantorUserKey, - ); + let ciphers: CipherView[] = []; + if (await this.configService.getFeatureFlag(FeatureFlag.PM4154_BulkEncryptionService)) { + ciphers = await this.bulkEncryptService.decryptItems( + response.ciphers.map((c) => new Cipher(c)), + grantorUserKey, + ); + } else { + ciphers = await this.encryptService.decryptItems( + response.ciphers.map((c) => new Cipher(c)), + grantorUserKey, + ); + } return ciphers.sort(this.cipherService.getLocaleSortingFunction()); } diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index fa53246062a..1b534b6f779 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -131,6 +131,7 @@ import { BraintreeService } from "@bitwarden/common/billing/services/payment-pro import { StripeService } from "@bitwarden/common/billing/services/payment-processors/stripe.service"; import { AppIdService as AppIdServiceAbstraction } from "@bitwarden/common/platform/abstractions/app-id.service"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; +import { BulkEncryptService } from "@bitwarden/common/platform/abstractions/bulk-encrypt.service"; import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto-function.service"; @@ -166,6 +167,7 @@ import { ConfigApiService } from "@bitwarden/common/platform/services/config/con import { DefaultConfigService } from "@bitwarden/common/platform/services/config/default-config.service"; import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service"; import { CryptoService } from "@bitwarden/common/platform/services/crypto.service"; +import { BulkEncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/bulk-encrypt.service.implementation"; import { MultithreadEncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/multithread-encrypt.service.implementation"; import { DefaultBroadcasterService } from "@bitwarden/common/platform/services/default-broadcaster.service"; import { DefaultEnvironmentService } from "@bitwarden/common/platform/services/default-environment.service"; @@ -437,6 +439,7 @@ const safeProviders: SafeProvider[] = [ stateService: StateServiceAbstraction, autofillSettingsService: AutofillSettingsServiceAbstraction, encryptService: EncryptService, + bulkEncryptService: BulkEncryptService, fileUploadService: CipherFileUploadServiceAbstraction, configService: ConfigService, stateProvider: StateProvider, @@ -450,6 +453,7 @@ const safeProviders: SafeProvider[] = [ stateService, autofillSettingsService, encryptService, + bulkEncryptService, fileUploadService, configService, stateProvider, @@ -463,6 +467,7 @@ const safeProviders: SafeProvider[] = [ StateServiceAbstraction, AutofillSettingsServiceAbstraction, EncryptService, + BulkEncryptService, CipherFileUploadServiceAbstraction, ConfigService, StateProvider, @@ -832,6 +837,11 @@ const safeProviders: SafeProvider[] = [ useClass: MultithreadEncryptServiceImplementation, deps: [CryptoFunctionServiceAbstraction, LogService, LOG_MAC_FAILURES], }), + safeProvider({ + provide: BulkEncryptService, + useClass: BulkEncryptServiceImplementation, + deps: [CryptoFunctionServiceAbstraction, LogService], + }), safeProvider({ provide: EventUploadServiceAbstraction, useClass: EventUploadService, diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index fb4bd1f9668..7e88af236fa 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -14,6 +14,7 @@ export enum FeatureFlag { EnableDeleteProvider = "AC-1218-delete-provider", ExtensionRefresh = "extension-refresh", RestrictProviderAccess = "restrict-provider-access", + PM4154_BulkEncryptionService = "PM-4154-bulk-encryption-service", UseTreeWalkerApiForPageDetailsCollection = "use-tree-walker-api-for-page-details-collection", EmailVerification = "email-verification", InlineMenuFieldQualification = "inline-menu-field-qualification", @@ -49,6 +50,7 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.EnableDeleteProvider]: FALSE, [FeatureFlag.ExtensionRefresh]: FALSE, [FeatureFlag.RestrictProviderAccess]: FALSE, + [FeatureFlag.PM4154_BulkEncryptionService]: FALSE, [FeatureFlag.UseTreeWalkerApiForPageDetailsCollection]: FALSE, [FeatureFlag.EmailVerification]: FALSE, [FeatureFlag.InlineMenuFieldQualification]: FALSE, diff --git a/libs/common/src/platform/abstractions/bulk-encrypt.service.ts b/libs/common/src/platform/abstractions/bulk-encrypt.service.ts new file mode 100644 index 00000000000..4cdff0c769a --- /dev/null +++ b/libs/common/src/platform/abstractions/bulk-encrypt.service.ts @@ -0,0 +1,10 @@ +import { Decryptable } from "../interfaces/decryptable.interface"; +import { InitializerMetadata } from "../interfaces/initializer-metadata.interface"; +import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key"; + +export abstract class BulkEncryptService { + abstract decryptItems( + items: Decryptable[], + key: SymmetricCryptoKey, + ): Promise; +} diff --git a/libs/common/src/platform/abstractions/encrypt.service.ts b/libs/common/src/platform/abstractions/encrypt.service.ts index bc526e35784..c70042e4186 100644 --- a/libs/common/src/platform/abstractions/encrypt.service.ts +++ b/libs/common/src/platform/abstractions/encrypt.service.ts @@ -13,6 +13,11 @@ export abstract class EncryptService { abstract rsaEncrypt(data: Uint8Array, publicKey: Uint8Array): Promise; abstract rsaDecrypt(data: EncString, privateKey: Uint8Array): Promise; abstract resolveLegacyKey(key: SymmetricCryptoKey, encThing: Encrypted): SymmetricCryptoKey; + /** + * @deprecated Replaced by BulkEncryptService, remove once the feature is tested and the featureflag PM-4154-multi-worker-encryption-service is removed + * @param items The items to decrypt + * @param key The key to decrypt the items with + */ abstract decryptItems( items: Decryptable[], key: SymmetricCryptoKey, diff --git a/libs/common/src/platform/services/cryptography/bulk-encrypt.service.implementation.ts b/libs/common/src/platform/services/cryptography/bulk-encrypt.service.implementation.ts new file mode 100644 index 00000000000..d3bbc82905a --- /dev/null +++ b/libs/common/src/platform/services/cryptography/bulk-encrypt.service.implementation.ts @@ -0,0 +1,164 @@ +import { firstValueFrom, fromEvent, filter, map, takeUntil, defaultIfEmpty, Subject } from "rxjs"; +import { Jsonify } from "type-fest"; + +import { BulkEncryptService } from "../../abstractions/bulk-encrypt.service"; +import { CryptoFunctionService } from "../../abstractions/crypto-function.service"; +import { LogService } from "../../abstractions/log.service"; +import { Decryptable } from "../../interfaces/decryptable.interface"; +import { InitializerMetadata } from "../../interfaces/initializer-metadata.interface"; +import { Utils } from "../../misc/utils"; +import { SymmetricCryptoKey } from "../../models/domain/symmetric-crypto-key"; + +import { getClassInitializer } from "./get-class-initializer"; + +// TTL (time to live) is not strictly required but avoids tying up memory resources if inactive +const workerTTL = 60000; // 1 minute +const maxWorkers = 8; +const minNumberOfItemsForMultithreading = 400; + +export class BulkEncryptServiceImplementation implements BulkEncryptService { + private workers: Worker[] = []; + private timeout: any; + + private clear$ = new Subject(); + + constructor( + protected cryptoFunctionService: CryptoFunctionService, + protected logService: LogService, + ) {} + + /** + * Decrypts items using a web worker if the environment supports it. + * Will fall back to the main thread if the window object is not available. + */ + async decryptItems( + items: Decryptable[], + key: SymmetricCryptoKey, + ): Promise { + if (key == null) { + throw new Error("No encryption key provided."); + } + + if (items == null || items.length < 1) { + return []; + } + + if (typeof window === "undefined") { + this.logService.info("Window not available in BulkEncryptService, decrypting sequentially"); + const results = []; + for (let i = 0; i < items.length; i++) { + results.push(await items[i].decrypt(key)); + } + return results; + } + + const decryptedItems = await this.getDecryptedItemsFromWorkers(items, key); + return decryptedItems; + } + + /** + * Sends items to a set of web workers to decrypt them. This utilizes multiple workers to decrypt items + * faster without interrupting other operations (e.g. updating UI). + */ + private async getDecryptedItemsFromWorkers( + items: Decryptable[], + key: SymmetricCryptoKey, + ): Promise { + if (items == null || items.length < 1) { + return []; + } + + this.clearTimeout(); + + const hardwareConcurrency = navigator.hardwareConcurrency || 1; + let numberOfWorkers = Math.min(hardwareConcurrency, maxWorkers); + if (items.length < minNumberOfItemsForMultithreading) { + numberOfWorkers = 1; + } + + this.logService.info( + `Starting decryption using multithreading with ${numberOfWorkers} workers for ${items.length} items`, + ); + + if (this.workers.length == 0) { + for (let i = 0; i < numberOfWorkers; i++) { + this.workers.push( + new Worker( + new URL( + /* webpackChunkName: 'encrypt-worker' */ + "@bitwarden/common/platform/services/cryptography/encrypt.worker.ts", + import.meta.url, + ), + ), + ); + } + } + + const itemsPerWorker = Math.floor(items.length / this.workers.length); + const results = []; + + for (const [i, worker] of this.workers.entries()) { + const start = i * itemsPerWorker; + const end = start + itemsPerWorker; + const itemsForWorker = items.slice(start, end); + + // push the remaining items to the last worker + if (i == this.workers.length - 1) { + itemsForWorker.push(...items.slice(end)); + } + + const request = { + id: Utils.newGuid(), + items: itemsForWorker, + key: key, + }; + + worker.postMessage(JSON.stringify(request)); + results.push( + firstValueFrom( + fromEvent(worker, "message").pipe( + filter((response: MessageEvent) => response.data?.id === request.id), + map((response) => JSON.parse(response.data.items)), + map((items) => + items.map((jsonItem: Jsonify) => { + const initializer = getClassInitializer(jsonItem.initializerKey); + return initializer(jsonItem); + }), + ), + takeUntil(this.clear$), + defaultIfEmpty([]), + ), + ), + ); + } + + const decryptedItems = (await Promise.all(results)).flat(); + this.logService.info( + `Finished decrypting ${decryptedItems.length} items using ${numberOfWorkers} workers`, + ); + + this.restartTimeout(); + + return decryptedItems; + } + + private clear() { + this.clear$.next(); + for (const worker of this.workers) { + worker.terminate(); + } + this.workers = []; + this.clearTimeout(); + } + + private restartTimeout() { + this.clearTimeout(); + this.timeout = setTimeout(() => this.clear(), workerTTL); + } + + private clearTimeout() { + if (this.timeout != null) { + clearTimeout(this.timeout); + } + } +} diff --git a/libs/common/src/platform/services/cryptography/encrypt.service.implementation.ts b/libs/common/src/platform/services/cryptography/encrypt.service.implementation.ts index 862ae2bc0e6..228f0c54174 100644 --- a/libs/common/src/platform/services/cryptography/encrypt.service.implementation.ts +++ b/libs/common/src/platform/services/cryptography/encrypt.service.implementation.ts @@ -185,6 +185,9 @@ export class EncryptServiceImplementation implements EncryptService { return this.cryptoFunctionService.rsaDecrypt(data.dataBytes, privateKey, algorithm); } + /** + * @deprecated Replaced by BulkEncryptService (PM-4154) + */ async decryptItems( items: Decryptable[], key: SymmetricCryptoKey, diff --git a/libs/common/src/platform/services/cryptography/fallback-bulk-encrypt.service.ts b/libs/common/src/platform/services/cryptography/fallback-bulk-encrypt.service.ts new file mode 100644 index 00000000000..44dc5a8bf76 --- /dev/null +++ b/libs/common/src/platform/services/cryptography/fallback-bulk-encrypt.service.ts @@ -0,0 +1,33 @@ +import { BulkEncryptService } from "../../abstractions/bulk-encrypt.service"; +import { EncryptService } from "../../abstractions/encrypt.service"; +import { Decryptable } from "../../interfaces/decryptable.interface"; +import { InitializerMetadata } from "../../interfaces/initializer-metadata.interface"; +import { SymmetricCryptoKey } from "../../models/domain/symmetric-crypto-key"; + +/** + * @deprecated For the feature flag from PM-4154, remove once feature is rolled out + */ +export class FallbackBulkEncryptService implements BulkEncryptService { + private featureFlagEncryptService: BulkEncryptService; + + constructor(protected encryptService: EncryptService) {} + + /** + * Decrypts items using a web worker if the environment supports it. + * Will fall back to the main thread if the window object is not available. + */ + async decryptItems( + items: Decryptable[], + key: SymmetricCryptoKey, + ): Promise { + if (this.featureFlagEncryptService != null) { + return await this.featureFlagEncryptService.decryptItems(items, key); + } else { + return await this.encryptService.decryptItems(items, key); + } + } + + async setFeatureFlagEncryptService(featureFlagEncryptService: BulkEncryptService) { + this.featureFlagEncryptService = featureFlagEncryptService; + } +} diff --git a/libs/common/src/platform/services/cryptography/multithread-encrypt.service.implementation.ts b/libs/common/src/platform/services/cryptography/multithread-encrypt.service.implementation.ts index 6ac343bcb6a..227db77526a 100644 --- a/libs/common/src/platform/services/cryptography/multithread-encrypt.service.implementation.ts +++ b/libs/common/src/platform/services/cryptography/multithread-encrypt.service.implementation.ts @@ -12,6 +12,9 @@ import { getClassInitializer } from "./get-class-initializer"; // TTL (time to live) is not strictly required but avoids tying up memory resources if inactive const workerTTL = 3 * 60000; // 3 minutes +/** + * @deprecated Replaced by BulkEncryptionService (PM-4154) + */ export class MultithreadEncryptServiceImplementation extends EncryptServiceImplementation { private worker: Worker; private timeout: any; diff --git a/libs/common/src/vault/services/cipher.service.spec.ts b/libs/common/src/vault/services/cipher.service.spec.ts index c84fd066b27..e3019ab48d2 100644 --- a/libs/common/src/vault/services/cipher.service.spec.ts +++ b/libs/common/src/vault/services/cipher.service.spec.ts @@ -1,6 +1,8 @@ import { mock } from "jest-mock-extended"; import { BehaviorSubject, of } from "rxjs"; +import { BulkEncryptService } from "@bitwarden/common/platform/abstractions/bulk-encrypt.service"; + import { FakeAccountService, mockAccountServiceWith } from "../../../spec/fake-account-service"; import { FakeStateProvider } from "../../../spec/fake-state-provider"; import { makeStaticByteArray } from "../../../spec/utils"; @@ -114,6 +116,7 @@ describe("Cipher Service", () => { const i18nService = mock(); const searchService = mock(); const encryptService = mock(); + const bulkEncryptService = mock(); const configService = mock(); accountService = mockAccountServiceWith(mockUserId); const stateProvider = new FakeStateProvider(accountService); @@ -136,6 +139,7 @@ describe("Cipher Service", () => { stateService, autofillSettingsService, encryptService, + bulkEncryptService, cipherFileUploadService, configService, stateProvider, diff --git a/libs/common/src/vault/services/cipher.service.ts b/libs/common/src/vault/services/cipher.service.ts index 1d06ae1dd0b..aa1d3a18279 100644 --- a/libs/common/src/vault/services/cipher.service.ts +++ b/libs/common/src/vault/services/cipher.service.ts @@ -1,6 +1,9 @@ import { firstValueFrom, map, Observable, skipWhile, switchMap } from "rxjs"; import { SemVer } from "semver"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { BulkEncryptService } from "@bitwarden/common/platform/abstractions/bulk-encrypt.service"; + import { ApiService } from "../../abstractions/api.service"; import { SearchService } from "../../abstractions/search.service"; import { AutofillSettingsServiceAbstraction } from "../../autofill/services/autofill-settings.service"; @@ -102,6 +105,7 @@ export class CipherService implements CipherServiceAbstraction { private stateService: StateService, private autofillSettingsService: AutofillSettingsServiceAbstraction, private encryptService: EncryptService, + private bulkEncryptService: BulkEncryptService, private cipherFileUploadService: CipherFileUploadService, private configService: ConfigService, private stateProvider: StateProvider, @@ -397,12 +401,19 @@ export class CipherService implements CipherServiceAbstraction { const decCiphers = ( await Promise.all( - Object.entries(grouped).map(([orgId, groupedCiphers]) => - this.encryptService.decryptItems( - groupedCiphers, - keys.orgKeys[orgId as OrganizationId] ?? keys.userKey, - ), - ), + Object.entries(grouped).map(async ([orgId, groupedCiphers]) => { + if (await this.configService.getFeatureFlag(FeatureFlag.PM4154_BulkEncryptionService)) { + return await this.bulkEncryptService.decryptItems( + groupedCiphers, + keys.orgKeys[orgId as OrganizationId] ?? keys.userKey, + ); + } else { + return await this.encryptService.decryptItems( + groupedCiphers, + keys.orgKeys[orgId as OrganizationId] ?? keys.userKey, + ); + } + }), ) ) .flat() @@ -515,7 +526,12 @@ export class CipherService implements CipherServiceAbstraction { const ciphers = response.data.map((cr) => new Cipher(new CipherData(cr))); const key = await this.cryptoService.getOrgKey(organizationId); - const decCiphers = await this.encryptService.decryptItems(ciphers, key); + let decCiphers: CipherView[] = []; + if (await this.configService.getFeatureFlag(FeatureFlag.PM4154_BulkEncryptionService)) { + decCiphers = await this.bulkEncryptService.decryptItems(ciphers, key); + } else { + decCiphers = await this.encryptService.decryptItems(ciphers, key); + } decCiphers.sort(this.getLocaleSortingFunction()); return decCiphers; From cebbb9486e2247533b500295b52e974ceedc487d Mon Sep 17 00:00:00 2001 From: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> Date: Thu, 18 Jul 2024 15:41:09 +0200 Subject: [PATCH 54/57] [PM-9855] Add premium badge to new file send item dropdown (#10137) * Create browsers SendV2 component * Add premium badge to new file send item dropdown --------- Co-authored-by: Daniel James Smith --- .../new-send-dropdown.component.html | 3 +++ .../new-send-dropdown.component.ts | 26 +++++++++++++++---- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/libs/tools/send/send-ui/src/new-send-dropdown/new-send-dropdown.component.html b/libs/tools/send/send-ui/src/new-send-dropdown/new-send-dropdown.component.html index 0766435e1ce..f1f0363c999 100644 --- a/libs/tools/send/send-ui/src/new-send-dropdown/new-send-dropdown.component.html +++ b/libs/tools/send/send-ui/src/new-send-dropdown/new-send-dropdown.component.html @@ -10,5 +10,8 @@ {{ "sendTypeFile" | i18n }} + diff --git a/libs/tools/send/send-ui/src/new-send-dropdown/new-send-dropdown.component.ts b/libs/tools/send/send-ui/src/new-send-dropdown/new-send-dropdown.component.ts index 1463b448a6a..620dc77c995 100644 --- a/libs/tools/send/send-ui/src/new-send-dropdown/new-send-dropdown.component.ts +++ b/libs/tools/send/send-ui/src/new-send-dropdown/new-send-dropdown.component.ts @@ -1,23 +1,39 @@ import { CommonModule } from "@angular/common"; -import { Component } from "@angular/core"; +import { Component, OnInit } from "@angular/core"; import { Router, RouterLink } from "@angular/router"; +import { firstValueFrom } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions"; import { SendType } from "@bitwarden/common/tools/send/enums/send-type"; -import { ButtonModule, MenuModule } from "@bitwarden/components"; +import { BadgeModule, ButtonModule, MenuModule } from "@bitwarden/components"; @Component({ selector: "tools-new-send-dropdown", templateUrl: "new-send-dropdown.component.html", standalone: true, - imports: [JslibModule, CommonModule, ButtonModule, RouterLink, MenuModule], + imports: [JslibModule, CommonModule, ButtonModule, RouterLink, MenuModule, BadgeModule], }) -export class NewSendDropdownComponent { +export class NewSendDropdownComponent implements OnInit { sendType = SendType; - constructor(private router: Router) {} + hasNoPremium = false; + + constructor( + private router: Router, + private billingAccountProfileStateService: BillingAccountProfileStateService, + ) {} + + async ngOnInit() { + this.hasNoPremium = !(await firstValueFrom( + this.billingAccountProfileStateService.hasPremiumFromAnySource$, + )); + } newItemNavigate(type: SendType) { + if (this.hasNoPremium && type === SendType.File) { + return this.router.navigate(["/premium"]); + } void this.router.navigate(["/add-send"], { queryParams: { type: type, isNew: true } }); } } From 9bfd838da696cf458b7c88837b4b6eb3b8c5eb49 Mon Sep 17 00:00:00 2001 From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> Date: Thu, 18 Jul 2024 08:53:53 -0500 Subject: [PATCH 55/57] [PM-8381] Assign collections (#9854) * initial add of assign collections component * grab cipherId from query params * add organization selection for moving a cipher * add multi-select for collections * add transfer of cipher to an organization * temp: do not show assign collections while a cipher already has an organization * account for initial collections for a cipher * block assign-collections route with feature flag * replace hardcoded string with i18n * separate out async calls to switchMap to avoid async subscribe * use local cipher rather than decrypting again * use anchor for better semantics * migrate form submission to bitSubmit directive * swap to "assign" rather than "save" * integrate with base AssignCollections component * clean up messages file * remove unneeded takeUntilDestroyed * remove unneeded bitFormButton * remove unused translations * lint fix * refactor assign-collections component to take in a button reference - This allows consuming components to not have to worry about loading/disabled states - The base AssignCollections component will change the submit button when supplied --- apps/browser/src/_locales/en/messages.json | 64 +++++++++++++-- .../src/popup/app-routing.animations.ts | 3 + apps/browser/src/popup/app-routing.module.ts | 7 ++ .../assign-collections.component.html | 31 +++++++ .../assign-collections.component.ts | 81 +++++++++++++++++++ .../item-more-options.component.html | 9 ++- .../assign-collections-web.component.html | 6 +- .../assign-collections-web.component.ts | 2 - .../assign-collections.component.html | 2 +- .../assign-collections.component.ts | 22 +++-- 10 files changed, 205 insertions(+), 22 deletions(-) create mode 100644 apps/browser/src/vault/popup/components/vault-v2/assign-collections/assign-collections.component.html create mode 100644 apps/browser/src/vault/popup/components/vault-v2/assign-collections/assign-collections.component.ts diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 5b306fdb2a7..b6968f1ff87 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -3544,12 +3544,6 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, - "itemDetails": { - "message": "Item details" - }, - "itemName": { - "message": "Item name" - }, "additionalInformation": { "message": "Additional information" }, @@ -3643,6 +3637,24 @@ "loading": { "message": "Loading" }, + "assign": { + "message": "Assign" + }, + "bulkCollectionAssignmentDialogDescription": { + "message": "Only organization members with access to these collections will be able to see the items." + }, + "bulkCollectionAssignmentWarning": { + "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2" + } + } + }, "addField": { "message": "Add field" }, @@ -3726,6 +3738,46 @@ } } }, + "selectCollectionsToAssign": { + "message": "Select collections to assign" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } + }, + "successfullyAssignedCollections": { + "message": "Successfully assigned collections" + }, + "nothingSelected": { + "message": "You have not selected anything." + }, + "movedItemsToOrg": { + "message": "Selected items moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, "reorderFieldDown":{ "message": "$LABEL$ moved down, position $INDEX$ of $LENGTH$", "placeholders": { diff --git a/apps/browser/src/popup/app-routing.animations.ts b/apps/browser/src/popup/app-routing.animations.ts index 065331bd414..227ede146ba 100644 --- a/apps/browser/src/popup/app-routing.animations.ts +++ b/apps/browser/src/popup/app-routing.animations.ts @@ -177,6 +177,9 @@ export const routerTransition = trigger("routerTransition", [ transition("tabs => account-security", inSlideLeft), transition("account-security => tabs", outSlideRight), + transition("tabs => assign-collections", inSlideLeft), + transition("assign-collections => tabs", outSlideRight), + // Vault settings transition("tabs => vault-settings", inSlideLeft), transition("vault-settings => tabs", outSlideRight), diff --git a/apps/browser/src/popup/app-routing.module.ts b/apps/browser/src/popup/app-routing.module.ts index 12d92249fed..53ab778c31d 100644 --- a/apps/browser/src/popup/app-routing.module.ts +++ b/apps/browser/src/popup/app-routing.module.ts @@ -71,6 +71,7 @@ import { VaultItemsComponent } from "../vault/popup/components/vault/vault-items import { VaultV2Component } from "../vault/popup/components/vault/vault-v2.component"; import { ViewComponent } from "../vault/popup/components/vault/view.component"; import { AddEditV2Component } from "../vault/popup/components/vault-v2/add-edit/add-edit-v2.component"; +import { AssignCollections } from "../vault/popup/components/vault-v2/assign-collections/assign-collections.component"; import { AttachmentsV2Component } from "../vault/popup/components/vault-v2/attachments/attachments-v2.component"; import { ViewV2Component } from "../vault/popup/components/vault-v2/view-v2/view-v2.component"; import { AppearanceComponent } from "../vault/popup/settings/appearance.component"; @@ -408,6 +409,12 @@ const routes: Routes = [ }, ], }, + { + path: "assign-collections", + component: AssignCollections, + canActivate: [canAccessFeature(FeatureFlag.ExtensionRefresh, true, "/")], + data: { state: "assign-collections" }, + }, ...extensionRefreshSwap(AboutPageComponent, AboutPageV2Component, { path: "about", canActivate: [AuthGuard], diff --git a/apps/browser/src/vault/popup/components/vault-v2/assign-collections/assign-collections.component.html b/apps/browser/src/vault/popup/components/vault-v2/assign-collections/assign-collections.component.html new file mode 100644 index 00000000000..8e8ce1f997c --- /dev/null +++ b/apps/browser/src/vault/popup/components/vault-v2/assign-collections/assign-collections.component.html @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + {{ "cancel" | i18n }} + + + diff --git a/apps/browser/src/vault/popup/components/vault-v2/assign-collections/assign-collections.component.ts b/apps/browser/src/vault/popup/components/vault-v2/assign-collections/assign-collections.component.ts new file mode 100644 index 00000000000..a3ebadb7e2b --- /dev/null +++ b/apps/browser/src/vault/popup/components/vault-v2/assign-collections/assign-collections.component.ts @@ -0,0 +1,81 @@ +import { CommonModule, Location } from "@angular/common"; +import { Component } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { ReactiveFormsModule } from "@angular/forms"; +import { ActivatedRoute } from "@angular/router"; +import { Observable, combineLatest, first, switchMap } from "rxjs"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { OrganizationId } from "@bitwarden/common/types/guid"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { + ButtonModule, + CardComponent, + SelectModule, + FormFieldModule, + AsyncActionsModule, +} from "@bitwarden/components"; +import { AssignCollectionsComponent, CollectionAssignmentParams } from "@bitwarden/vault"; + +import { PopOutComponent } from "../../../../../platform/popup/components/pop-out.component"; +import { PopupFooterComponent } from "../../../../../platform/popup/layout/popup-footer.component"; +import { PopupHeaderComponent } from "../../../../../platform/popup/layout/popup-header.component"; +import { PopupPageComponent } from "../../../../../platform/popup/layout/popup-page.component"; + +@Component({ + standalone: true, + selector: "app-assign-collections", + templateUrl: "./assign-collections.component.html", + imports: [ + AsyncActionsModule, + ButtonModule, + CommonModule, + JslibModule, + SelectModule, + FormFieldModule, + AssignCollectionsComponent, + CardComponent, + ReactiveFormsModule, + PopupPageComponent, + PopupHeaderComponent, + PopupFooterComponent, + PopOutComponent, + ], +}) +export class AssignCollections { + /** Params needed to populate the assign collections component */ + params: CollectionAssignmentParams; + + constructor( + private location: Location, + private collectionService: CollectionService, + private cipherService: CipherService, + route: ActivatedRoute, + ) { + const $cipher: Observable = route.queryParams.pipe( + switchMap(({ cipherId }) => this.cipherService.get(cipherId)), + switchMap((cipherDomain) => + this.cipherService + .getKeyForCipherKeyDecryption(cipherDomain) + .then(cipherDomain.decrypt.bind(cipherDomain)), + ), + ); + + combineLatest([$cipher, this.collectionService.decryptedCollections$]) + .pipe(takeUntilDestroyed(), first()) + .subscribe(([cipherView, collections]) => { + this.params = { + ciphers: [cipherView], + organizationId: (cipherView?.organizationId as OrganizationId) ?? null, + availableCollections: collections.filter((c) => !c.readOnly), + }; + }); + } + + /** Navigates the user back to the previous screen */ + navigateBack() { + this.location.back(); + } +} diff --git a/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.html b/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.html index 05a6b54d4d0..0a0f44e8e00 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.html +++ b/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.html @@ -28,9 +28,14 @@ {{ "clone" | i18n }} - + diff --git a/apps/web/src/app/vault/components/assign-collections/assign-collections-web.component.html b/apps/web/src/app/vault/components/assign-collections/assign-collections-web.component.html index f05262832c7..4f5b6234ad9 100644 --- a/apps/web/src/app/vault/components/assign-collections/assign-collections-web.component.html +++ b/apps/web/src/app/vault/components/assign-collections/assign-collections-web.component.html @@ -9,8 +9,7 @@
@@ -18,8 +17,7 @@