From e262441999e4e243f903c8a781fcefc7906fa60c Mon Sep 17 00:00:00 2001 From: Dave <3836813+enmande@users.noreply.github.com> Date: Tue, 17 Feb 2026 17:43:00 -0500 Subject: [PATCH 01/89] [PM-31088] saltForUser should emit salt from master password unlock data (#18976) * feat(salt-for-user) [PM-31088]: Add feature flag for saltForUser. * feat(salt-for-user) [PM-31088]: Flag saltForUser logic to return unlockdata.salt or emailToSalt. * test(salt-for-user) [PM-31088]: Update tests to include coverage for new behavior. --- libs/common/src/enums/feature-flag.enum.ts | 2 + .../services/master-password.service.spec.ts | 47 +++++++++++++++++-- .../services/master-password.service.ts | 30 ++++++++++-- 3 files changed, 72 insertions(+), 7 deletions(-) diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index 05fded6bcaf..71b95ec6057 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -19,6 +19,7 @@ export enum FeatureFlag { PM23801_PrefetchPasswordPrelogin = "pm-23801-prefetch-password-prelogin", PM27086_UpdateAuthenticationApisForInputPassword = "pm-27086-update-authentication-apis-for-input-password", SafariAccountSwitching = "pm-5594-safari-account-switching", + PM31088_MasterPasswordServiceEmitSalt = "pm-31088-master-password-service-emit-salt", /* Autofill */ UseUndeterminedCipherScenarioTriggeringLogic = "undetermined-cipher-scenario-logic", @@ -143,6 +144,7 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.PM23801_PrefetchPasswordPrelogin]: FALSE, [FeatureFlag.PM27086_UpdateAuthenticationApisForInputPassword]: FALSE, [FeatureFlag.SafariAccountSwitching]: FALSE, + [FeatureFlag.PM31088_MasterPasswordServiceEmitSalt]: FALSE, /* Billing */ [FeatureFlag.TrialPaymentOptional]: FALSE, diff --git a/libs/common/src/key-management/master-password/services/master-password.service.spec.ts b/libs/common/src/key-management/master-password/services/master-password.service.spec.ts index f72ae0e7c5e..4a96dedf024 100644 --- a/libs/common/src/key-management/master-password/services/master-password.service.spec.ts +++ b/libs/common/src/key-management/master-password/services/master-password.service.spec.ts @@ -17,8 +17,11 @@ import { mockAccountServiceWith, } from "../../../../spec"; import { ForceSetPasswordReason } from "../../../auth/models/domain/force-set-password-reason"; +import { FeatureFlag } from "../../../enums/feature-flag.enum"; +import { ServerConfig } from "../../../platform/abstractions/config/server-config"; import { LogService } from "../../../platform/abstractions/log.service"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; +import { USER_SERVER_CONFIG } from "../../../platform/services/config/default-config.service"; import { UserId } from "../../../types/guid"; import { MasterKey, UserKey } from "../../../types/key"; import { KeyGenerationService } from "../../crypto"; @@ -92,14 +95,52 @@ describe("MasterPasswordService", () => { sut.saltForUser$(null as unknown as UserId); }).toThrow("userId is null or undefined."); }); + // Removable with unwinding of PM31088_MasterPasswordServiceEmitSalt it("throws when userid present but not in account service", async () => { await expect( firstValueFrom(sut.saltForUser$("00000000-0000-0000-0000-000000000001" as UserId)), ).rejects.toThrow("Cannot read properties of undefined (reading 'email')"); }); - it("returns salt", async () => { - const salt = await firstValueFrom(sut.saltForUser$(userId)); - expect(salt).toBeDefined(); + // Removable with unwinding of PM31088_MasterPasswordServiceEmitSalt + it("returns email-derived salt for legacy path", async () => { + const result = await firstValueFrom(sut.saltForUser$(userId)); + // mockAccountServiceWith defaults email to "email" + expect(result).toBe("email" as MasterPasswordSalt); + }); + + describe("saltForUser$ master password unlock data migration path", () => { + // Flagged with PM31088_MasterPasswordServiceEmitSalt PM-31088 + beforeEach(() => { + stateProvider.singleUser.getFake(userId, USER_SERVER_CONFIG).nextState({ + featureStates: { + [FeatureFlag.PM31088_MasterPasswordServiceEmitSalt]: true, + }, + } as unknown as ServerConfig); + }); + + // Unwinding should promote these tests as part of saltForUser suite. + it("returns salt from master password unlock data", async () => { + const expectedSalt = "custom-salt" as MasterPasswordSalt; + const unlockData = new MasterPasswordUnlockData( + expectedSalt, + new PBKDF2KdfConfig(600_000), + makeEncString().toSdk() as MasterKeyWrappedUserKey, + ); + stateProvider.singleUser + .getFake(userId, MASTER_PASSWORD_UNLOCK_KEY) + .nextState(unlockData.toJSON()); + + const result = await firstValueFrom(sut.saltForUser$(userId)); + expect(result).toBe(expectedSalt); + }); + + it("throws when master password unlock data is null", async () => { + stateProvider.singleUser.getFake(userId, MASTER_PASSWORD_UNLOCK_KEY).nextState(null); + + await expect(firstValueFrom(sut.saltForUser$(userId))).rejects.toThrow( + "Master password unlock data not found for user.", + ); + }); }); }); diff --git a/libs/common/src/key-management/master-password/services/master-password.service.ts b/libs/common/src/key-management/master-password/services/master-password.service.ts index 28d4f58d7dc..f1a074ff14c 100644 --- a/libs/common/src/key-management/master-password/services/master-password.service.ts +++ b/libs/common/src/key-management/master-password/services/master-password.service.ts @@ -1,6 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { firstValueFrom, map, Observable } from "rxjs"; +import { firstValueFrom, iif, map, Observable, switchMap } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { assertNonNullish } from "@bitwarden/common/auth/utils"; @@ -12,8 +12,10 @@ import { KdfConfig } from "@bitwarden/key-management"; import { PureCrypto } from "@bitwarden/sdk-internal"; import { ForceSetPasswordReason } from "../../../auth/models/domain/force-set-password-reason"; +import { FeatureFlag, getFeatureFlagValue } from "../../../enums/feature-flag.enum"; import { LogService } from "../../../platform/abstractions/log.service"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; +import { USER_SERVER_CONFIG } from "../../../platform/services/config/default-config.service"; import { MASTER_PASSWORD_DISK, MASTER_PASSWORD_MEMORY, @@ -102,9 +104,29 @@ export class MasterPasswordService implements InternalMasterPasswordServiceAbstr saltForUser$(userId: UserId): Observable { assertNonNullish(userId, "userId"); - return this.accountService.accounts$.pipe( - map((accounts) => accounts[userId].email), - map((email) => this.emailToSalt(email)), + + // Note: We can't use the config service as an abstraction here because it creates a circular dependency: ConfigService -> ConfigApiService -> ApiService -> VaultTimeoutSettingsService -> KeyService -> MP service. + return this.stateProvider.getUser(userId, USER_SERVER_CONFIG).state$.pipe( + map((serverConfig) => + getFeatureFlagValue(serverConfig, FeatureFlag.PM31088_MasterPasswordServiceEmitSalt), + ), + switchMap((enabled) => + iif( + () => enabled, + this.masterPasswordUnlockData$(userId).pipe( + map((unlockData) => { + if (unlockData == null) { + throw new Error("Master password unlock data not found for user."); + } + return unlockData.salt; + }), + ), + this.accountService.accounts$.pipe( + map((accounts) => accounts[userId].email), + map((email) => this.emailToSalt(email)), + ), + ), + ), ); } From 24c3b8fb2bfb51abb3b7674b1e000577f71f289d Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Tue, 17 Feb 2026 16:43:23 -0800 Subject: [PATCH 02/89] fix autofill on click behavior (#19046) --- .../autofill-vault-list-items.component.html | 2 +- .../vault-list-items-container.component.ts | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/browser/src/vault/popup/components/vault/autofill-vault-list-items/autofill-vault-list-items.component.html b/apps/browser/src/vault/popup/components/vault/autofill-vault-list-items/autofill-vault-list-items.component.html index 38d60233200..8ea65e77c5e 100644 --- a/apps/browser/src/vault/popup/components/vault/autofill-vault-list-items/autofill-vault-list-items.component.html +++ b/apps/browser/src/vault/popup/components/vault/autofill-vault-list-items/autofill-vault-list-items.component.html @@ -6,8 +6,8 @@ (onRefresh)="refreshCurrentTab()" [description]="(showEmptyAutofillTip$ | async) ? ('autofillSuggestionsTip' | i18n) : undefined" isAutofillList + showAutofillButton [disableDescriptionMargin]="showEmptyAutofillTip$ | async" [groupByType]="groupByType()" - [showAutofillButton]="(clickItemsToAutofillVaultView$ | async) === false" [primaryActionAutofill]="clickItemsToAutofillVaultView$ | async" > diff --git a/apps/browser/src/vault/popup/components/vault/vault-list-items-container/vault-list-items-container.component.ts b/apps/browser/src/vault/popup/components/vault/vault-list-items-container/vault-list-items-container.component.ts index fb8d20c5cf6..331ea799169 100644 --- a/apps/browser/src/vault/popup/components/vault/vault-list-items-container/vault-list-items-container.component.ts +++ b/apps/browser/src/vault/popup/components/vault/vault-list-items-container/vault-list-items-container.component.ts @@ -302,8 +302,9 @@ export class VaultListItemsContainerComponent implements AfterViewInit { if (this.currentUriIsBlocked()) { return false; } - return this.isAutofillList() - ? this.simplifiedItemActionEnabled() + + return this.simplifiedItemActionEnabled() + ? this.isAutofillList() : this.primaryActionAutofill(); }); From ff775c7bbc4867a7d42c8bfceaa5acce1c570f9b Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Tue, 17 Feb 2026 16:43:37 -0800 Subject: [PATCH 03/89] fix click on "Fill" text (#19047) --- .../vault-list-items-container.component.html | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/apps/browser/src/vault/popup/components/vault/vault-list-items-container/vault-list-items-container.component.html b/apps/browser/src/vault/popup/components/vault/vault-list-items-container/vault-list-items-container.component.html index e9e89776dde..69c548540eb 100644 --- a/apps/browser/src/vault/popup/components/vault/vault-list-items-container/vault-list-items-container.component.html +++ b/apps/browser/src/vault/popup/components/vault/vault-list-items-container/vault-list-items-container.component.html @@ -90,7 +90,13 @@ - + } @if (showAutofillBadge()) { From ec33ea4f3c661050f458677aa1ecd7773be234df Mon Sep 17 00:00:00 2001 From: Alex <55413326+AlexRubik@users.noreply.github.com> Date: Tue, 17 Feb 2026 21:29:41 -0700 Subject: [PATCH 04/89] [PM-27782] Update Access Intelligence loading state text (#18808) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [PM-27782] Update Access Intelligence loading state text Simplify the loading progress messages shown during Access Intelligence report generation to be more user-friendly and concise. Changes: - Add new i18n keys with simplified text - Update ProgressStepConfig to use new keys Progress message updates: - "Fetching member data..." → "Reviewing member data..." - "Analyzing password health..." → "Analyzing passwords..." - "Calculating risk scores..." → "Calculating risks..." - "Generating report data..." → "Generating reports..." - "Saving report..." → "Compiling insights..." - "Compiling insights..." → "Done!" * delete old messages * remove all "this might take a few minutes" --- apps/web/src/locales/en/messages.json | 37 +++++++++---------- .../shared/report-loading.component.html | 13 ++----- .../shared/report-loading.component.ts | 12 +++--- 3 files changed, 27 insertions(+), 35 deletions(-) diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 970244119f8..cc73a04b81b 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -4596,29 +4596,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/report-loading.component.html b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/report-loading.component.html index 0b5a63c8f03..c816861b623 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/report-loading.component.html +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/report-loading.component.html @@ -10,14 +10,9 @@ > - -
- - {{ stepConfig[progressStep()].message | i18n }} - - - {{ "thisMightTakeFewMinutes" | i18n }} - -
+ + + {{ stepConfig[progressStep()].message | i18n }} + diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/report-loading.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/report-loading.component.ts index 45b28dae470..9df729b9645 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/report-loading.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/report-loading.component.ts @@ -6,12 +6,12 @@ import { ProgressModule } from "@bitwarden/components"; // Map of progress step to display config const ProgressStepConfig = Object.freeze({ - [ReportProgress.FetchingMembers]: { message: "fetchingMemberData", progress: 20 }, - [ReportProgress.AnalyzingPasswords]: { message: "analyzingPasswordHealth", progress: 40 }, - [ReportProgress.CalculatingRisks]: { message: "calculatingRiskScores", progress: 60 }, - [ReportProgress.GeneratingReport]: { message: "generatingReportData", progress: 80 }, - [ReportProgress.Saving]: { message: "savingReport", progress: 95 }, - [ReportProgress.Complete]: { message: "compilingInsights", progress: 100 }, + [ReportProgress.FetchingMembers]: { message: "reviewingMemberData", progress: 20 }, + [ReportProgress.AnalyzingPasswords]: { message: "analyzingPasswords", progress: 40 }, + [ReportProgress.CalculatingRisks]: { message: "calculatingRisks", progress: 60 }, + [ReportProgress.GeneratingReport]: { message: "generatingReports", progress: 80 }, + [ReportProgress.Saving]: { message: "compilingInsightsProgress", progress: 95 }, + [ReportProgress.Complete]: { message: "reportGenerationDone", progress: 100 }, } as const); // FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush From 03340aee7102f4c296b0e83e732bff7d7f14cf1c Mon Sep 17 00:00:00 2001 From: Alex <55413326+AlexRubik@users.noreply.github.com> Date: Tue, 17 Feb 2026 21:31:08 -0700 Subject: [PATCH 05/89] [PM-31163] stabilize table column widths with fixed layout (#18708) * stabilize table column widths with fixed layout (PM-31163) Add layout="fixed" and explicit width classes to report tables to prevent column widths from shifting during virtual scroll. Files changed: - weak-passwords-report.component.html - reused-passwords-report.component.html - exposed-passwords-report.component.html - inactive-two-factor-report.component.html - unsecured-websites-report.component.html * use auto width for name column to fix width calculation (PM-31163) Remove tw-w-1/2 from name column headers. With layout="fixed", the explicit percentages didn't sum to 100%, causing inconsistent column widths. Before: | 48px | 50% | 25% | 25% | = 48px + 100% (overflow) After: | 48px | auto | 25% | 25% | = columns sum correctly Name column now uses auto to fill remaining space. * render headers in Admin Console to fix column widths (PM-31163) Admin Console reports had a very wide icon column because no headers were rendered. Without headers, table-layout: fixed uses data row content to determine column widths, causing inconsistent sizing. Root cause: Three reports had their entire block inside @if (!isAdminConsoleActive), so when isAdminConsoleActive=true (Admin Console), no headers were rendered at all. Before (broken): @if (!isAdminConsoleActive) { Icon Name Owner } After (fixed): Icon Name @if (!isAdminConsoleActive) { Owner } This matches the pattern already used by weak-passwords-report and exposed-passwords-report, which were working correctly. Files changed: - unsecured-websites-report.component.html - reused-passwords-report.component.html - inactive-two-factor-report.component.html Result: - Admin Console now renders headers with correct column widths - Icon column is 48px (tw-w-12) as expected - Owner column properly hidden in Admin Console view * truncate long item names to prevent column overflow - you can hover cursor for tooltip to see full name --- .../exposed-passwords-report.component.html | 12 ++--- .../inactive-two-factor-report.component.html | 46 ++++++++++-------- .../reused-passwords-report.component.html | 48 ++++++++++--------- .../unsecured-websites-report.component.html | 46 ++++++++++-------- .../weak-passwords-report.component.html | 10 ++-- 5 files changed, 87 insertions(+), 75 deletions(-) diff --git a/apps/web/src/app/dirt/reports/pages/exposed-passwords-report.component.html b/apps/web/src/app/dirt/reports/pages/exposed-passwords-report.component.html index 144396d6772..56316fcddee 100644 --- a/apps/web/src/app/dirt/reports/pages/exposed-passwords-report.component.html +++ b/apps/web/src/app/dirt/reports/pages/exposed-passwords-report.component.html @@ -43,16 +43,16 @@ > } } - + - + {{ "name" | i18n }} @if (!isAdminConsoleActive) { - + {{ "owner" | i18n }} } - + {{ "timesExposed" | i18n }} @@ -60,7 +60,7 @@ - + @if (!organization || canManageCipher(row)) { } @else { - {{ row.name }} + {{ row.name }} } @if (!organization && row.organizationId) { } } - - @if (!isAdminConsoleActive) { - - - {{ "name" | i18n }} - {{ "owner" | i18n }} - - - } + + + + {{ "name" | i18n }} + @if (!isAdminConsoleActive) { + {{ "owner" | i18n }} + } + + - + @if (!organization || canManageCipher(row)) { {{ row.name }} } @else { - {{ row.name }} + {{ row.name }} } @if (!organization && row.organizationId) { {{ row.subTitle }} - - @if (!organization) { - - } - + @if (!isAdminConsoleActive) { + + @if (!organization) { + + } + + } @if (cipherDocs.has(row.id)) { diff --git a/apps/web/src/app/dirt/reports/pages/reused-passwords-report.component.html b/apps/web/src/app/dirt/reports/pages/reused-passwords-report.component.html index f08af8bda01..66bd11e7bc3 100644 --- a/apps/web/src/app/dirt/reports/pages/reused-passwords-report.component.html +++ b/apps/web/src/app/dirt/reports/pages/reused-passwords-report.component.html @@ -45,20 +45,20 @@ > } } - - @if (!isAdminConsoleActive) { - - - {{ "name" | i18n }} - {{ "owner" | i18n }} - {{ "timesReused" | i18n }} - - } + + + + {{ "name" | i18n }} + @if (!isAdminConsoleActive) { + {{ "owner" | i18n }} + } + {{ "timesReused" | i18n }} + - + @if (!organization || canManageCipher(row)) { {{ row.name }} } @else { - {{ row.name }} + {{ row.name }} } @if (!organization && row.organizationId) { {{ row.subTitle }} - - @if (!organization) { - - - } - + @if (!isAdminConsoleActive) { + + @if (!organization) { + + + } + + } {{ "reusedXTimes" | i18n: passwordUseMap.get(row.login.password) }} diff --git a/apps/web/src/app/dirt/reports/pages/unsecured-websites-report.component.html b/apps/web/src/app/dirt/reports/pages/unsecured-websites-report.component.html index 810c1e384b0..553c3f2f04e 100644 --- a/apps/web/src/app/dirt/reports/pages/unsecured-websites-report.component.html +++ b/apps/web/src/app/dirt/reports/pages/unsecured-websites-report.component.html @@ -45,19 +45,19 @@ > } } - - @if (!isAdminConsoleActive) { - - - {{ "name" | i18n }} - {{ "owner" | i18n }} - - } + + + + {{ "name" | i18n }} + @if (!isAdminConsoleActive) { + {{ "owner" | i18n }} + } + - + @if (!organization || canManageCipher(row)) { {{ row.name }} } @else { - {{ row.name }} + {{ row.name }} } @if (!organization && row.organizationId) { {{ row.subTitle }} - - @if (!organization) { - - - } - + @if (!isAdminConsoleActive) { + + @if (!organization) { + + + } + + } } diff --git a/apps/web/src/app/dirt/reports/pages/weak-passwords-report.component.html b/apps/web/src/app/dirt/reports/pages/weak-passwords-report.component.html index 5a187427b5e..fd5b916e661 100644 --- a/apps/web/src/app/dirt/reports/pages/weak-passwords-report.component.html +++ b/apps/web/src/app/dirt/reports/pages/weak-passwords-report.component.html @@ -45,12 +45,12 @@ > } } - + - + {{ "name" | i18n }} @if (!isAdminConsoleActive) { - + {{ "owner" | i18n }} } @@ -62,7 +62,7 @@ - + @if (!organization || canManageCipher(row)) { {{ row.name }} } @else { - {{ row.name }} + {{ row.name }} } @if (!organization && row.organizationId) { Date: Wed, 18 Feb 2026 09:32:08 +0100 Subject: [PATCH 06/89] Fix non-relative imports (#19022) --- tsconfig.base.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tsconfig.base.json b/tsconfig.base.json index 17f8f6d44fc..995eac031fd 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -24,8 +24,8 @@ "@bitwarden/assets/svg": ["./libs/assets/src/svg/index.ts"], "@bitwarden/auth/angular": ["./libs/auth/src/angular"], "@bitwarden/auth/common": ["./libs/auth/src/common"], - "@bitwarden/auto-confirm": ["libs/auto-confirm/src/index.ts"], - "@bitwarden/auto-confirm/angular": ["libs/auto-confirm/src/angular"], + "@bitwarden/auto-confirm": ["./libs/auto-confirm/src/index.ts"], + "@bitwarden/auto-confirm/angular": ["./libs/auto-confirm/src/angular"], "@bitwarden/billing": ["./libs/billing/src"], "@bitwarden/bit-common/*": ["./bitwarden_license/bit-common/src/*"], "@bitwarden/browser/*": ["./apps/browser/src/*"], From cf5e19463937c72f48bc3e8275558181de854bd2 Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Wed, 18 Feb 2026 06:57:29 -0600 Subject: [PATCH 07/89] [BRE-1621] Fix Appx Release (#19043) * Revert to electron-builder appx manifest template * Remove comments * Remove unnecessary namespaces * Re-include Tamil translation files * Reinstate bitwarden protocol handler * Set minimum version to Windows 10 2016 Anniversary Update * Fix spacing --- apps/desktop/custom-appx-manifest.xml | 25 ++++++++++++----------- apps/desktop/electron-builder.beta.json | 1 - apps/desktop/electron-builder.json | 1 - apps/desktop/scripts/appx-cross-build.ps1 | 3 ++- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/apps/desktop/custom-appx-manifest.xml b/apps/desktop/custom-appx-manifest.xml index 166b852588b..8a5c36e7da6 100644 --- a/apps/desktop/custom-appx-manifest.xml +++ b/apps/desktop/custom-appx-manifest.xml @@ -1,17 +1,9 @@ - + xmlns:desktop="http://schemas.microsoft.com/appx/manifest/desktop/windows10" + xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"> + @@ -87,8 +80,9 @@ xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/re - + + @@ -106,6 +100,13 @@ xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/re + + + + Bitwarden + + + diff --git a/apps/desktop/electron-builder.beta.json b/apps/desktop/electron-builder.beta.json index 9c66b17aa1f..f0746e6d408 100644 --- a/apps/desktop/electron-builder.beta.json +++ b/apps/desktop/electron-builder.beta.json @@ -61,7 +61,6 @@ "appx": { "artifactName": "Bitwarden-Beta-${version}-${arch}.${ext}", "backgroundColor": "#175DDC", - "customManifestPath": "./custom-appx-manifest.xml", "applicationId": "BitwardenBeta", "identityName": "8bitSolutionsLLC.BitwardenBeta", "publisher": "CN=14D52771-DE3C-4886-B8BF-825BA7690418", diff --git a/apps/desktop/electron-builder.json b/apps/desktop/electron-builder.json index 151ce72182d..f876b7ff680 100644 --- a/apps/desktop/electron-builder.json +++ b/apps/desktop/electron-builder.json @@ -176,7 +176,6 @@ "appx": { "artifactName": "${productName}-${version}-${arch}.${ext}", "backgroundColor": "#175DDC", - "customManifestPath": "./custom-appx-manifest.xml", "applicationId": "bitwardendesktop", "identityName": "8bitSolutionsLLC.bitwardendesktop", "publisher": "CN=14D52771-DE3C-4886-B8BF-825BA7690418", diff --git a/apps/desktop/scripts/appx-cross-build.ps1 b/apps/desktop/scripts/appx-cross-build.ps1 index ef2ab09104c..c47567695ed 100755 --- a/apps/desktop/scripts/appx-cross-build.ps1 +++ b/apps/desktop/scripts/appx-cross-build.ps1 @@ -72,6 +72,7 @@ param( # Whether to build in release mode. $Release=$false ) + $ErrorActionPreference = "Stop" $PSNativeCommandUseErrorActionPreference = $true $startTime = Get-Date @@ -113,7 +114,7 @@ else { $builderConfig = Get-Content $electronConfigFile | ConvertFrom-Json $packageConfig = Get-Content package.json | ConvertFrom-Json -$manifestTemplate = Get-Content $builderConfig.appx.customManifestPath +$manifestTemplate = Get-Content ($builderConfig.appx.customManifestPath ?? "custom-appx-manifest.xml") $srcDir = Get-Location $assetsDir = Get-Item $builderConfig.directories.buildResources From 51731c1526470bb20a4eb46a46a15c73b6746599 Mon Sep 17 00:00:00 2001 From: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> Date: Wed, 18 Feb 2026 14:32:21 +0000 Subject: [PATCH 08/89] Bumped client version(s) --- apps/desktop/package.json | 2 +- apps/desktop/src/package-lock.json | 4 ++-- apps/desktop/src/package.json | 2 +- package-lock.json | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/desktop/package.json b/apps/desktop/package.json index cd2147d21e4..5718c752a7c 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": "2026.2.0", + "version": "2026.2.1", "keywords": [ "bitwarden", "password", diff --git a/apps/desktop/src/package-lock.json b/apps/desktop/src/package-lock.json index 0aa188eba2f..01c429ab3d0 100644 --- a/apps/desktop/src/package-lock.json +++ b/apps/desktop/src/package-lock.json @@ -1,12 +1,12 @@ { "name": "@bitwarden/desktop", - "version": "2026.2.0", + "version": "2026.2.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@bitwarden/desktop", - "version": "2026.2.0", + "version": "2026.2.1", "license": "GPL-3.0", "dependencies": { "@bitwarden/desktop-napi": "file:../desktop_native/napi" diff --git a/apps/desktop/src/package.json b/apps/desktop/src/package.json index 0076981ab60..fac797b5344 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": "2026.2.0", + "version": "2026.2.1", "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 8d3c32c027d..bf532fba66a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -232,7 +232,7 @@ }, "apps/desktop": { "name": "@bitwarden/desktop", - "version": "2026.2.0", + "version": "2026.2.1", "hasInstallScript": true, "license": "GPL-3.0" }, From 5161a232f52cd714ad64b5ff6b4b42d4e07a43c6 Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Wed, 18 Feb 2026 09:06:10 -0600 Subject: [PATCH 09/89] [PM-29055] Remove pm-25379-use-new-organization-metadata-structure feature flag (#18848) Remove the fully-enabled feature flag and simplify the billing metadata API to always use the vNext endpoints. The legacy API path is removed since the server will no longer serve it. - Remove FeatureFlag.PM25379_UseNewOrganizationMetadataStructure enum and default - Delete legacy getOrganizationBillingMetadata() API method (old /billing/metadata path) - Rename vNext methods to remove VNext suffix - Simplify OrganizationMetadataService to always use cached vNext path - Remove ConfigService dependency from OrganizationMetadataService - Update tests to remove feature flag branching --- .../src/services/jslib-services.module.ts | 2 +- .../billing-api.service.abstraction.ts | 6 +- .../billing/services/billing-api.service.ts | 16 +- .../organization-metadata.service.spec.ts | 264 ++++++------------ .../organization-metadata.service.ts | 57 +--- libs/common/src/enums/feature-flag.enum.ts | 2 - 6 files changed, 98 insertions(+), 249 deletions(-) diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 9d407f0f310..02ec9833d6f 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -1528,7 +1528,7 @@ const safeProviders: SafeProvider[] = [ safeProvider({ provide: OrganizationMetadataServiceAbstraction, useClass: DefaultOrganizationMetadataService, - deps: [BillingApiServiceAbstraction, ConfigService, PlatformUtilsServiceAbstraction], + deps: [BillingApiServiceAbstraction, PlatformUtilsServiceAbstraction], }), safeProvider({ provide: BillingAccountProfileStateService, diff --git a/libs/common/src/billing/abstractions/billing-api.service.abstraction.ts b/libs/common/src/billing/abstractions/billing-api.service.abstraction.ts index dcb395ef85c..9868a57bd78 100644 --- a/libs/common/src/billing/abstractions/billing-api.service.abstraction.ts +++ b/libs/common/src/billing/abstractions/billing-api.service.abstraction.ts @@ -21,11 +21,7 @@ export abstract class BillingApiServiceAbstraction { organizationId: OrganizationId, ): Promise; - abstract getOrganizationBillingMetadataVNext( - organizationId: OrganizationId, - ): Promise; - - abstract getOrganizationBillingMetadataVNextSelfHost( + abstract getOrganizationBillingMetadataSelfHost( organizationId: OrganizationId, ): Promise; diff --git a/libs/common/src/billing/services/billing-api.service.ts b/libs/common/src/billing/services/billing-api.service.ts index ae6913e545c..834606426db 100644 --- a/libs/common/src/billing/services/billing-api.service.ts +++ b/libs/common/src/billing/services/billing-api.service.ts @@ -36,20 +36,6 @@ export class BillingApiService implements BillingApiServiceAbstraction { async getOrganizationBillingMetadata( organizationId: OrganizationId, - ): Promise { - const r = await this.apiService.send( - "GET", - "/organizations/" + organizationId + "/billing/metadata", - null, - true, - true, - ); - - return new OrganizationBillingMetadataResponse(r); - } - - async getOrganizationBillingMetadataVNext( - organizationId: OrganizationId, ): Promise { const r = await this.apiService.send( "GET", @@ -62,7 +48,7 @@ export class BillingApiService implements BillingApiServiceAbstraction { return new OrganizationBillingMetadataResponse(r); } - async getOrganizationBillingMetadataVNextSelfHost( + async getOrganizationBillingMetadataSelfHost( organizationId: OrganizationId, ): Promise { const r = await this.apiService.send( diff --git a/libs/common/src/billing/services/organization/organization-metadata.service.spec.ts b/libs/common/src/billing/services/organization/organization-metadata.service.spec.ts index a2b012eb161..998356cbc14 100644 --- a/libs/common/src/billing/services/organization/organization-metadata.service.spec.ts +++ b/libs/common/src/billing/services/organization/organization-metadata.service.spec.ts @@ -1,13 +1,11 @@ import { mock } from "jest-mock-extended"; -import { BehaviorSubject, firstValueFrom } from "rxjs"; +import { firstValueFrom } from "rxjs"; import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions"; import { OrganizationBillingMetadataResponse } from "@bitwarden/common/billing/models/response/organization-billing-metadata.response"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { newGuid } from "@bitwarden/guid"; -import { FeatureFlag } from "../../../enums/feature-flag.enum"; import { OrganizationId } from "../../../types/guid"; import { DefaultOrganizationMetadataService } from "./organization-metadata.service"; @@ -15,9 +13,7 @@ import { DefaultOrganizationMetadataService } from "./organization-metadata.serv describe("DefaultOrganizationMetadataService", () => { let service: DefaultOrganizationMetadataService; let billingApiService: jest.Mocked; - let configService: jest.Mocked; let platformUtilsService: jest.Mocked; - let featureFlagSubject: BehaviorSubject; const mockOrganizationId = newGuid() as OrganizationId; const mockOrganizationId2 = newGuid() as OrganizationId; @@ -34,182 +30,114 @@ describe("DefaultOrganizationMetadataService", () => { beforeEach(() => { billingApiService = mock(); - configService = mock(); platformUtilsService = mock(); - featureFlagSubject = new BehaviorSubject(false); - configService.getFeatureFlag$.mockReturnValue(featureFlagSubject.asObservable()); platformUtilsService.isSelfHost.mockReturnValue(false); - service = new DefaultOrganizationMetadataService( - billingApiService, - configService, - platformUtilsService, - ); + service = new DefaultOrganizationMetadataService(billingApiService, platformUtilsService); }); afterEach(() => { jest.resetAllMocks(); - featureFlagSubject.complete(); }); describe("getOrganizationMetadata$", () => { - describe("feature flag OFF", () => { - beforeEach(() => { - featureFlagSubject.next(false); - }); + it("calls getOrganizationBillingMetadata for cloud-hosted", async () => { + const mockResponse = createMockMetadataResponse(false, 10); + billingApiService.getOrganizationBillingMetadata.mockResolvedValue(mockResponse); - it("calls getOrganizationBillingMetadata when feature flag is off", async () => { - const mockResponse = createMockMetadataResponse(false, 10); - billingApiService.getOrganizationBillingMetadata.mockResolvedValue(mockResponse); + const result = await firstValueFrom(service.getOrganizationMetadata$(mockOrganizationId)); - const result = await firstValueFrom(service.getOrganizationMetadata$(mockOrganizationId)); - - expect(configService.getFeatureFlag$).toHaveBeenCalledWith( - FeatureFlag.PM25379_UseNewOrganizationMetadataStructure, - ); - expect(billingApiService.getOrganizationBillingMetadata).toHaveBeenCalledWith( - mockOrganizationId, - ); - expect(billingApiService.getOrganizationBillingMetadataVNext).not.toHaveBeenCalled(); - expect(result).toEqual(mockResponse); - }); - - it("does not cache metadata when feature flag is off", async () => { - const mockResponse1 = createMockMetadataResponse(false, 10); - const mockResponse2 = createMockMetadataResponse(false, 15); - billingApiService.getOrganizationBillingMetadata - .mockResolvedValueOnce(mockResponse1) - .mockResolvedValueOnce(mockResponse2); - - const result1 = await firstValueFrom(service.getOrganizationMetadata$(mockOrganizationId)); - const result2 = await firstValueFrom(service.getOrganizationMetadata$(mockOrganizationId)); - - expect(billingApiService.getOrganizationBillingMetadata).toHaveBeenCalledTimes(2); - expect(result1).toEqual(mockResponse1); - expect(result2).toEqual(mockResponse2); - }); + expect(billingApiService.getOrganizationBillingMetadata).toHaveBeenCalledWith( + mockOrganizationId, + ); + expect(result).toEqual(mockResponse); }); - describe("feature flag ON", () => { - beforeEach(() => { - featureFlagSubject.next(true); - }); + it("calls getOrganizationBillingMetadataSelfHost when isSelfHost is true", async () => { + platformUtilsService.isSelfHost.mockReturnValue(true); + const mockResponse = createMockMetadataResponse(true, 25); + billingApiService.getOrganizationBillingMetadataSelfHost.mockResolvedValue(mockResponse); - it("calls getOrganizationBillingMetadataVNext when feature flag is on", async () => { - const mockResponse = createMockMetadataResponse(true, 15); - billingApiService.getOrganizationBillingMetadataVNext.mockResolvedValue(mockResponse); + const result = await firstValueFrom(service.getOrganizationMetadata$(mockOrganizationId)); - const result = await firstValueFrom(service.getOrganizationMetadata$(mockOrganizationId)); - - expect(configService.getFeatureFlag$).toHaveBeenCalledWith( - FeatureFlag.PM25379_UseNewOrganizationMetadataStructure, - ); - expect(billingApiService.getOrganizationBillingMetadataVNext).toHaveBeenCalledWith( - mockOrganizationId, - ); - expect(billingApiService.getOrganizationBillingMetadata).not.toHaveBeenCalled(); - expect(result).toEqual(mockResponse); - }); - - it("caches metadata by organization ID when feature flag is on", async () => { - const mockResponse = createMockMetadataResponse(true, 10); - billingApiService.getOrganizationBillingMetadataVNext.mockResolvedValue(mockResponse); - - const result1 = await firstValueFrom(service.getOrganizationMetadata$(mockOrganizationId)); - const result2 = await firstValueFrom(service.getOrganizationMetadata$(mockOrganizationId)); - - expect(billingApiService.getOrganizationBillingMetadataVNext).toHaveBeenCalledTimes(1); - expect(result1).toEqual(mockResponse); - expect(result2).toEqual(mockResponse); - }); - - it("maintains separate cache entries for different organization IDs", async () => { - const mockResponse1 = createMockMetadataResponse(true, 10); - const mockResponse2 = createMockMetadataResponse(false, 20); - billingApiService.getOrganizationBillingMetadataVNext - .mockResolvedValueOnce(mockResponse1) - .mockResolvedValueOnce(mockResponse2); - - const result1 = await firstValueFrom(service.getOrganizationMetadata$(mockOrganizationId)); - const result2 = await firstValueFrom(service.getOrganizationMetadata$(mockOrganizationId2)); - const result3 = await firstValueFrom(service.getOrganizationMetadata$(mockOrganizationId)); - const result4 = await firstValueFrom(service.getOrganizationMetadata$(mockOrganizationId2)); - - expect(billingApiService.getOrganizationBillingMetadataVNext).toHaveBeenCalledTimes(2); - expect(billingApiService.getOrganizationBillingMetadataVNext).toHaveBeenNthCalledWith( - 1, - mockOrganizationId, - ); - expect(billingApiService.getOrganizationBillingMetadataVNext).toHaveBeenNthCalledWith( - 2, - mockOrganizationId2, - ); - expect(result1).toEqual(mockResponse1); - expect(result2).toEqual(mockResponse2); - expect(result3).toEqual(mockResponse1); - expect(result4).toEqual(mockResponse2); - }); - - it("calls getOrganizationBillingMetadataVNextSelfHost when feature flag is on and isSelfHost is true", async () => { - platformUtilsService.isSelfHost.mockReturnValue(true); - const mockResponse = createMockMetadataResponse(true, 25); - billingApiService.getOrganizationBillingMetadataVNextSelfHost.mockResolvedValue( - mockResponse, - ); - - const result = await firstValueFrom(service.getOrganizationMetadata$(mockOrganizationId)); - - expect(platformUtilsService.isSelfHost).toHaveBeenCalled(); - expect(billingApiService.getOrganizationBillingMetadataVNextSelfHost).toHaveBeenCalledWith( - mockOrganizationId, - ); - expect(billingApiService.getOrganizationBillingMetadataVNext).not.toHaveBeenCalled(); - expect(billingApiService.getOrganizationBillingMetadata).not.toHaveBeenCalled(); - expect(result).toEqual(mockResponse); - }); + expect(platformUtilsService.isSelfHost).toHaveBeenCalled(); + expect(billingApiService.getOrganizationBillingMetadataSelfHost).toHaveBeenCalledWith( + mockOrganizationId, + ); + expect(billingApiService.getOrganizationBillingMetadata).not.toHaveBeenCalled(); + expect(result).toEqual(mockResponse); }); - describe("shareReplay behavior", () => { - beforeEach(() => { - featureFlagSubject.next(true); - }); + it("caches metadata by organization ID", async () => { + const mockResponse = createMockMetadataResponse(true, 10); + billingApiService.getOrganizationBillingMetadata.mockResolvedValue(mockResponse); - it("does not call API multiple times when the same cached observable is subscribed to multiple times", async () => { - const mockResponse = createMockMetadataResponse(true, 10); - billingApiService.getOrganizationBillingMetadataVNext.mockResolvedValue(mockResponse); + const result1 = await firstValueFrom(service.getOrganizationMetadata$(mockOrganizationId)); + const result2 = await firstValueFrom(service.getOrganizationMetadata$(mockOrganizationId)); - const metadata$ = service.getOrganizationMetadata$(mockOrganizationId); + expect(billingApiService.getOrganizationBillingMetadata).toHaveBeenCalledTimes(1); + expect(result1).toEqual(mockResponse); + expect(result2).toEqual(mockResponse); + }); - const subscription1Promise = firstValueFrom(metadata$); - const subscription2Promise = firstValueFrom(metadata$); - const subscription3Promise = firstValueFrom(metadata$); + it("maintains separate cache entries for different organization IDs", async () => { + const mockResponse1 = createMockMetadataResponse(true, 10); + const mockResponse2 = createMockMetadataResponse(false, 20); + billingApiService.getOrganizationBillingMetadata + .mockResolvedValueOnce(mockResponse1) + .mockResolvedValueOnce(mockResponse2); - const [result1, result2, result3] = await Promise.all([ - subscription1Promise, - subscription2Promise, - subscription3Promise, - ]); + const result1 = await firstValueFrom(service.getOrganizationMetadata$(mockOrganizationId)); + const result2 = await firstValueFrom(service.getOrganizationMetadata$(mockOrganizationId2)); + const result3 = await firstValueFrom(service.getOrganizationMetadata$(mockOrganizationId)); + const result4 = await firstValueFrom(service.getOrganizationMetadata$(mockOrganizationId2)); - expect(billingApiService.getOrganizationBillingMetadataVNext).toHaveBeenCalledTimes(1); - expect(result1).toEqual(mockResponse); - expect(result2).toEqual(mockResponse); - expect(result3).toEqual(mockResponse); - }); + expect(billingApiService.getOrganizationBillingMetadata).toHaveBeenCalledTimes(2); + expect(billingApiService.getOrganizationBillingMetadata).toHaveBeenNthCalledWith( + 1, + mockOrganizationId, + ); + expect(billingApiService.getOrganizationBillingMetadata).toHaveBeenNthCalledWith( + 2, + mockOrganizationId2, + ); + expect(result1).toEqual(mockResponse1); + expect(result2).toEqual(mockResponse2); + expect(result3).toEqual(mockResponse1); + expect(result4).toEqual(mockResponse2); + }); + + it("does not call API multiple times when the same cached observable is subscribed to multiple times", async () => { + const mockResponse = createMockMetadataResponse(true, 10); + billingApiService.getOrganizationBillingMetadata.mockResolvedValue(mockResponse); + + const metadata$ = service.getOrganizationMetadata$(mockOrganizationId); + + const subscription1Promise = firstValueFrom(metadata$); + const subscription2Promise = firstValueFrom(metadata$); + const subscription3Promise = firstValueFrom(metadata$); + + const [result1, result2, result3] = await Promise.all([ + subscription1Promise, + subscription2Promise, + subscription3Promise, + ]); + + expect(billingApiService.getOrganizationBillingMetadata).toHaveBeenCalledTimes(1); + expect(result1).toEqual(mockResponse); + expect(result2).toEqual(mockResponse); + expect(result3).toEqual(mockResponse); }); }); describe("refreshMetadataCache", () => { - beforeEach(() => { - featureFlagSubject.next(true); - }); - - it("refreshes cached metadata when called with feature flag on", (done) => { + it("refreshes cached metadata when called", (done) => { const mockResponse1 = createMockMetadataResponse(true, 10); const mockResponse2 = createMockMetadataResponse(true, 20); let invocationCount = 0; - billingApiService.getOrganizationBillingMetadataVNext + billingApiService.getOrganizationBillingMetadata .mockResolvedValueOnce(mockResponse1) .mockResolvedValueOnce(mockResponse2); @@ -221,7 +149,7 @@ describe("DefaultOrganizationMetadataService", () => { expect(result).toEqual(mockResponse1); } else if (invocationCount === 2) { expect(result).toEqual(mockResponse2); - expect(billingApiService.getOrganizationBillingMetadataVNext).toHaveBeenCalledTimes(2); + expect(billingApiService.getOrganizationBillingMetadata).toHaveBeenCalledTimes(2); subscription.unsubscribe(); done(); } @@ -234,45 +162,13 @@ describe("DefaultOrganizationMetadataService", () => { }, 10); }); - it("does trigger refresh when feature flag is disabled", async () => { - featureFlagSubject.next(false); - - const mockResponse1 = createMockMetadataResponse(false, 10); - const mockResponse2 = createMockMetadataResponse(false, 20); - let invocationCount = 0; - - billingApiService.getOrganizationBillingMetadata - .mockResolvedValueOnce(mockResponse1) - .mockResolvedValueOnce(mockResponse2); - - const subscription = service.getOrganizationMetadata$(mockOrganizationId).subscribe({ - next: () => { - invocationCount++; - }, - }); - - // wait for initial invocation - await new Promise((resolve) => setTimeout(resolve, 10)); - - expect(invocationCount).toBe(1); - - service.refreshMetadataCache(); - - await new Promise((resolve) => setTimeout(resolve, 10)); - - expect(invocationCount).toBe(2); - expect(billingApiService.getOrganizationBillingMetadata).toHaveBeenCalledTimes(2); - - subscription.unsubscribe(); - }); - it("bypasses cache when refreshing metadata", (done) => { const mockResponse1 = createMockMetadataResponse(true, 10); const mockResponse2 = createMockMetadataResponse(true, 20); const mockResponse3 = createMockMetadataResponse(true, 30); let invocationCount = 0; - billingApiService.getOrganizationBillingMetadataVNext + billingApiService.getOrganizationBillingMetadata .mockResolvedValueOnce(mockResponse1) .mockResolvedValueOnce(mockResponse2) .mockResolvedValueOnce(mockResponse3); @@ -289,7 +185,7 @@ describe("DefaultOrganizationMetadataService", () => { service.refreshMetadataCache(); } else if (invocationCount === 3) { expect(result).toEqual(mockResponse3); - expect(billingApiService.getOrganizationBillingMetadataVNext).toHaveBeenCalledTimes(3); + expect(billingApiService.getOrganizationBillingMetadata).toHaveBeenCalledTimes(3); subscription.unsubscribe(); done(); } diff --git a/libs/common/src/billing/services/organization/organization-metadata.service.ts b/libs/common/src/billing/services/organization/organization-metadata.service.ts index 5ce87262c4b..149c4536df4 100644 --- a/libs/common/src/billing/services/organization/organization-metadata.service.ts +++ b/libs/common/src/billing/services/organization/organization-metadata.service.ts @@ -1,10 +1,8 @@ -import { BehaviorSubject, combineLatest, from, Observable, shareReplay, switchMap } from "rxjs"; +import { BehaviorSubject, from, Observable, shareReplay, switchMap } from "rxjs"; import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { FeatureFlag } from "../../../enums/feature-flag.enum"; -import { ConfigService } from "../../../platform/abstractions/config/config.service"; import { OrganizationId } from "../../../types/guid"; import { OrganizationMetadataServiceAbstraction } from "../../abstractions/organization-metadata.service.abstraction"; import { OrganizationBillingMetadataResponse } from "../../models/response/organization-billing-metadata.response"; @@ -17,7 +15,6 @@ export class DefaultOrganizationMetadataService implements OrganizationMetadataS constructor( private billingApiService: BillingApiServiceAbstraction, - private configService: ConfigService, private platformUtilsService: PlatformUtilsService, ) {} private refreshMetadataTrigger = new BehaviorSubject(undefined); @@ -28,50 +25,26 @@ export class DefaultOrganizationMetadataService implements OrganizationMetadataS }; getOrganizationMetadata$(orgId: OrganizationId): Observable { - return combineLatest([ - this.refreshMetadataTrigger, - this.configService.getFeatureFlag$(FeatureFlag.PM25379_UseNewOrganizationMetadataStructure), - ]).pipe( - switchMap(([_, featureFlagEnabled]) => - featureFlagEnabled - ? this.vNextGetOrganizationMetadataInternal$(orgId) - : this.getOrganizationMetadataInternal$(orgId), - ), - ); - } - - private vNextGetOrganizationMetadataInternal$( - orgId: OrganizationId, - ): Observable { - const cacheHit = this.metadataCache.get(orgId); - if (cacheHit) { - return cacheHit; - } - - const result = from(this.fetchMetadata(orgId, true)).pipe( - shareReplay({ bufferSize: 1, refCount: false }), - ); - - this.metadataCache.set(orgId, result); - return result; - } - - private getOrganizationMetadataInternal$( - organizationId: OrganizationId, - ): Observable { - return from(this.fetchMetadata(organizationId, false)).pipe( - shareReplay({ bufferSize: 1, refCount: false }), + return this.refreshMetadataTrigger.pipe( + switchMap(() => { + const cacheHit = this.metadataCache.get(orgId); + if (cacheHit) { + return cacheHit; + } + const result = from(this.fetchMetadata(orgId)).pipe( + shareReplay({ bufferSize: 1, refCount: false }), + ); + this.metadataCache.set(orgId, result); + return result; + }), ); } private async fetchMetadata( organizationId: OrganizationId, - featureFlagEnabled: boolean, ): Promise { - return featureFlagEnabled - ? this.platformUtilsService.isSelfHost() - ? await this.billingApiService.getOrganizationBillingMetadataVNextSelfHost(organizationId) - : await this.billingApiService.getOrganizationBillingMetadataVNext(organizationId) + return this.platformUtilsService.isSelfHost() + ? await this.billingApiService.getOrganizationBillingMetadataSelfHost(organizationId) : await this.billingApiService.getOrganizationBillingMetadata(organizationId); } } diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index 71b95ec6057..d252f7dcda5 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -31,7 +31,6 @@ export enum FeatureFlag { /* Billing */ TrialPaymentOptional = "PM-8163-trial-payment", PM24032_NewNavigationPremiumUpgradeButton = "pm-24032-new-navigation-premium-upgrade-button", - PM25379_UseNewOrganizationMetadataStructure = "pm-25379-use-new-organization-metadata-structure", PM26793_FetchPremiumPriceFromPricingService = "pm-26793-fetch-premium-price-from-pricing-service", PM23713_PremiumBadgeOpensNewPremiumUpgradeDialog = "pm-23713-premium-badge-opens-new-premium-upgrade-dialog", PM26462_Milestone_3 = "pm-26462-milestone-3", @@ -149,7 +148,6 @@ export const DefaultFeatureFlagValue = { /* Billing */ [FeatureFlag.TrialPaymentOptional]: FALSE, [FeatureFlag.PM24032_NewNavigationPremiumUpgradeButton]: FALSE, - [FeatureFlag.PM25379_UseNewOrganizationMetadataStructure]: FALSE, [FeatureFlag.PM26793_FetchPremiumPriceFromPricingService]: FALSE, [FeatureFlag.PM23713_PremiumBadgeOpensNewPremiumUpgradeDialog]: FALSE, [FeatureFlag.PM26462_Milestone_3]: FALSE, From dda862a8c6924d7e202be91f032b6c9c5037d085 Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Wed, 18 Feb 2026 09:39:58 -0600 Subject: [PATCH 10/89] Revert "Bumped client version(s)" (#19062) This reverts commit 51731c1526470bb20a4eb46a46a15c73b6746599. The desktop version was bumped erroneously, skipping 2026.2.0. --- apps/desktop/package.json | 2 +- apps/desktop/src/package-lock.json | 4 ++-- apps/desktop/src/package.json | 2 +- package-lock.json | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 5718c752a7c..cd2147d21e4 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": "2026.2.1", + "version": "2026.2.0", "keywords": [ "bitwarden", "password", diff --git a/apps/desktop/src/package-lock.json b/apps/desktop/src/package-lock.json index 01c429ab3d0..0aa188eba2f 100644 --- a/apps/desktop/src/package-lock.json +++ b/apps/desktop/src/package-lock.json @@ -1,12 +1,12 @@ { "name": "@bitwarden/desktop", - "version": "2026.2.1", + "version": "2026.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@bitwarden/desktop", - "version": "2026.2.1", + "version": "2026.2.0", "license": "GPL-3.0", "dependencies": { "@bitwarden/desktop-napi": "file:../desktop_native/napi" diff --git a/apps/desktop/src/package.json b/apps/desktop/src/package.json index fac797b5344..0076981ab60 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": "2026.2.1", + "version": "2026.2.0", "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 bf532fba66a..8d3c32c027d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -232,7 +232,7 @@ }, "apps/desktop": { "name": "@bitwarden/desktop", - "version": "2026.2.1", + "version": "2026.2.0", "hasInstallScript": true, "license": "GPL-3.0" }, From 1ef8f257b0120fbef5f22e1828e2facea9a9913e Mon Sep 17 00:00:00 2001 From: Alex <55413326+AlexRubik@users.noreply.github.com> Date: Wed, 18 Feb 2026 09:00:36 -0700 Subject: [PATCH 11/89] [PM-31803] Fix Password Manager reports not displaying items with limited collection access (#18956) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When "Owners and admins can manage all collections and items" is OFF, Password Manager reports incorrectly filter out items from collections where the user has "Can view", "Can view except passwords", or "Can edit except passwords" access. The root cause is that all five PM report components filter ciphers using `(!this.organization && !edit) || !viewPassword`. Since PM reports run without an organization context (this.organization is undefined), this condition excludes any item where edit=false or viewPassword=false. These permission checks are unnecessary for PM reports because: 1. Personal vault items always have edit=true and viewPassword=true, so the checks never applied to them. 2. Organization items should appear in reports regardless of permission level — the user has collection access, and edit restrictions should only affect the item dialog, not report visibility. 3. Admin Console reports (which work correctly) skip this filtering because this.organization is always set, making the condition always false. This also explains why "Can edit except passwords" items only appeared in the Unsecured Websites report — it was the only report that didn't check !viewPassword. Removed the edit/viewPassword filter conditions from all five PM report components: - exposed-passwords-report - weak-passwords-report - reused-passwords-report - inactive-two-factor-report - unsecured-websites-report --- ...exposed-passwords-report.component.spec.ts | 15 +++++------- .../exposed-passwords-report.component.ts | 6 ++--- ...active-two-factor-report.component.spec.ts | 23 ++++++++----------- .../inactive-two-factor-report.component.ts | 6 ++--- .../reused-passwords-report.component.spec.ts | 14 +++++------ .../reused-passwords-report.component.ts | 6 ++--- ...nsecured-websites-report.component.spec.ts | 11 ++++----- .../unsecured-websites-report.component.ts | 7 +----- .../weak-passwords-report.component.spec.ts | 15 +++++------- .../pages/weak-passwords-report.component.ts | 11 ++------- 10 files changed, 41 insertions(+), 73 deletions(-) diff --git a/apps/web/src/app/dirt/reports/pages/exposed-passwords-report.component.spec.ts b/apps/web/src/app/dirt/reports/pages/exposed-passwords-report.component.spec.ts index e056ec44af5..81e4a78b491 100644 --- a/apps/web/src/app/dirt/reports/pages/exposed-passwords-report.component.spec.ts +++ b/apps/web/src/app/dirt/reports/pages/exposed-passwords-report.component.spec.ts @@ -122,19 +122,16 @@ describe("ExposedPasswordsReportComponent", () => { expect(component).toBeTruthy(); }); - it('should get only ciphers with exposed passwords that the user has "Can Edit" access to', async () => { - const expectedIdOne: any = "cbea34a8-bde4-46ad-9d19-b05001228ab2"; - const expectedIdTwo = "cbea34a8-bde4-46ad-9d19-b05001228cd3"; - + it("should get ciphers with exposed passwords regardless of edit access", async () => { jest.spyOn(auditService, "passwordLeaked").mockReturnValue(Promise.resolve(1234)); jest.spyOn(component as any, "getAllCiphers").mockReturnValue(Promise.resolve(cipherData)); await component.setCiphers(); - expect(component.ciphers.length).toEqual(2); - expect(component.ciphers[0].id).toEqual(expectedIdOne); - expect(component.ciphers[0].edit).toEqual(true); - expect(component.ciphers[1].id).toEqual(expectedIdTwo); - expect(component.ciphers[1].edit).toEqual(true); + const cipherIds = component.ciphers.map((c) => c.id); + expect(cipherIds).toContain("cbea34a8-bde4-46ad-9d19-b05001228ab1"); + expect(cipherIds).toContain("cbea34a8-bde4-46ad-9d19-b05001228ab2"); + expect(cipherIds).toContain("cbea34a8-bde4-46ad-9d19-b05001228cd3"); + expect(component.ciphers.length).toEqual(3); }); it("should call fullSync method of syncService", () => { diff --git a/apps/web/src/app/dirt/reports/pages/exposed-passwords-report.component.ts b/apps/web/src/app/dirt/reports/pages/exposed-passwords-report.component.ts index 51bdde3eda8..e39ef811d66 100644 --- a/apps/web/src/app/dirt/reports/pages/exposed-passwords-report.component.ts +++ b/apps/web/src/app/dirt/reports/pages/exposed-passwords-report.component.ts @@ -64,14 +64,12 @@ export class ExposedPasswordsReportComponent extends CipherReportComponent imple this.filterStatus = [0]; allCiphers.forEach((ciph) => { - const { type, login, isDeleted, edit, viewPassword } = ciph; + const { type, login, isDeleted } = ciph; if ( type !== CipherType.Login || login.password == null || login.password === "" || - isDeleted || - (!this.organization && !edit) || - !viewPassword + isDeleted ) { return; } diff --git a/apps/web/src/app/dirt/reports/pages/inactive-two-factor-report.component.spec.ts b/apps/web/src/app/dirt/reports/pages/inactive-two-factor-report.component.spec.ts index 12453ea3b88..07a772755f5 100644 --- a/apps/web/src/app/dirt/reports/pages/inactive-two-factor-report.component.spec.ts +++ b/apps/web/src/app/dirt/reports/pages/inactive-two-factor-report.component.spec.ts @@ -95,9 +95,7 @@ describe("InactiveTwoFactorReportComponent", () => { expect(component).toBeTruthy(); }); - it('should get only ciphers with domains in the 2fa directory that they have "Can Edit" access to', async () => { - const expectedIdOne: any = "cbea34a8-bde4-46ad-9d19-b05001228xy4"; - const expectedIdTwo: any = "cbea34a8-bde4-46ad-9d19-b05001227nm5"; + it("should get ciphers with domains in the 2fa directory regardless of edit access", async () => { component.services.set( "101domain.com", "https://help.101domain.com/account-management/account-security/enabling-disabling-two-factor-verification", @@ -110,11 +108,10 @@ describe("InactiveTwoFactorReportComponent", () => { jest.spyOn(component as any, "getAllCiphers").mockReturnValue(Promise.resolve(cipherData)); await component.setCiphers(); + const cipherIds = component.ciphers.map((c) => c.id); + expect(cipherIds).toContain("cbea34a8-bde4-46ad-9d19-b05001228xy4"); + expect(cipherIds).toContain("cbea34a8-bde4-46ad-9d19-b05001227nm5"); expect(component.ciphers.length).toEqual(2); - expect(component.ciphers[0].id).toEqual(expectedIdOne); - expect(component.ciphers[0].edit).toEqual(true); - expect(component.ciphers[1].id).toEqual(expectedIdTwo); - expect(component.ciphers[1].edit).toEqual(true); }); it("should call fullSync method of syncService", () => { @@ -197,7 +194,7 @@ describe("InactiveTwoFactorReportComponent", () => { expect(doc).toBe(""); }); - it("should return false if cipher does not have edit access and no organization", () => { + it("should return true for cipher without edit access", () => { component.organization = null; const cipher = createCipherView({ edit: false, @@ -206,11 +203,11 @@ describe("InactiveTwoFactorReportComponent", () => { }, }); const [doc, isInactive] = (component as any).isInactive2faCipher(cipher); - expect(isInactive).toBe(false); - expect(doc).toBe(""); + expect(isInactive).toBe(true); + expect(doc).toBe("https://example.com/2fa-doc"); }); - it("should return false if cipher does not have viewPassword", () => { + it("should return true for cipher without viewPassword", () => { const cipher = createCipherView({ viewPassword: false, login: { @@ -218,8 +215,8 @@ describe("InactiveTwoFactorReportComponent", () => { }, }); const [doc, isInactive] = (component as any).isInactive2faCipher(cipher); - expect(isInactive).toBe(false); - expect(doc).toBe(""); + expect(isInactive).toBe(true); + expect(doc).toBe("https://example.com/2fa-doc"); }); it("should check all uris and return true if any matches domain or host", () => { diff --git a/apps/web/src/app/dirt/reports/pages/inactive-two-factor-report.component.ts b/apps/web/src/app/dirt/reports/pages/inactive-two-factor-report.component.ts index 9d7de688f3e..cd892130518 100644 --- a/apps/web/src/app/dirt/reports/pages/inactive-two-factor-report.component.ts +++ b/apps/web/src/app/dirt/reports/pages/inactive-two-factor-report.component.ts @@ -92,14 +92,12 @@ export class InactiveTwoFactorReportComponent extends CipherReportComponent impl let docFor2fa: string = ""; let isInactive2faCipher: boolean = false; - const { type, login, isDeleted, edit, viewPassword } = cipher; + const { type, login, isDeleted } = cipher; if ( type !== CipherType.Login || (login.totp != null && login.totp !== "") || !login.hasUris || - isDeleted || - (!this.organization && !edit) || - !viewPassword + isDeleted ) { return [docFor2fa, isInactive2faCipher]; } diff --git a/apps/web/src/app/dirt/reports/pages/reused-passwords-report.component.spec.ts b/apps/web/src/app/dirt/reports/pages/reused-passwords-report.component.spec.ts index 1b7006d0c68..8f08d06e27b 100644 --- a/apps/web/src/app/dirt/reports/pages/reused-passwords-report.component.spec.ts +++ b/apps/web/src/app/dirt/reports/pages/reused-passwords-report.component.spec.ts @@ -109,17 +109,15 @@ describe("ReusedPasswordsReportComponent", () => { expect(component).toBeTruthy(); }); - it('should get ciphers with reused passwords that the user has "Can Edit" access to', async () => { - const expectedIdOne: any = "cbea34a8-bde4-46ad-9d19-b05001228ab2"; - const expectedIdTwo = "cbea34a8-bde4-46ad-9d19-b05001228cd3"; + it("should get ciphers with reused passwords regardless of edit access", async () => { jest.spyOn(component as any, "getAllCiphers").mockReturnValue(Promise.resolve(cipherData)); await component.setCiphers(); - expect(component.ciphers.length).toEqual(2); - expect(component.ciphers[0].id).toEqual(expectedIdOne); - expect(component.ciphers[0].edit).toEqual(true); - expect(component.ciphers[1].id).toEqual(expectedIdTwo); - expect(component.ciphers[1].edit).toEqual(true); + const cipherIds = component.ciphers.map((c) => c.id); + expect(cipherIds).toContain("cbea34a8-bde4-46ad-9d19-b05001228ab1"); + expect(cipherIds).toContain("cbea34a8-bde4-46ad-9d19-b05001228ab2"); + expect(cipherIds).toContain("cbea34a8-bde4-46ad-9d19-b05001228cd3"); + expect(component.ciphers.length).toEqual(3); }); it("should call fullSync method of syncService", () => { diff --git a/apps/web/src/app/dirt/reports/pages/reused-passwords-report.component.ts b/apps/web/src/app/dirt/reports/pages/reused-passwords-report.component.ts index 0a81b19d4ff..7d24e61f276 100644 --- a/apps/web/src/app/dirt/reports/pages/reused-passwords-report.component.ts +++ b/apps/web/src/app/dirt/reports/pages/reused-passwords-report.component.ts @@ -71,14 +71,12 @@ export class ReusedPasswordsReportComponent extends CipherReportComponent implem this.filterStatus = [0]; ciphers.forEach((ciph) => { - const { type, login, isDeleted, edit, viewPassword } = ciph; + const { type, login, isDeleted } = ciph; if ( type !== CipherType.Login || login.password == null || login.password === "" || - isDeleted || - (!this.organization && !edit) || - !viewPassword + isDeleted ) { return; } diff --git a/apps/web/src/app/dirt/reports/pages/unsecured-websites-report.component.spec.ts b/apps/web/src/app/dirt/reports/pages/unsecured-websites-report.component.spec.ts index 2107e0c8df7..f116faf114f 100644 --- a/apps/web/src/app/dirt/reports/pages/unsecured-websites-report.component.spec.ts +++ b/apps/web/src/app/dirt/reports/pages/unsecured-websites-report.component.spec.ts @@ -118,17 +118,14 @@ describe("UnsecuredWebsitesReportComponent", () => { expect(component).toBeTruthy(); }); - it('should get only unsecured ciphers that the user has "Can Edit" access to', async () => { - const expectedIdOne: any = "cbea34a8-bde4-46ad-9d19-b05001228ab2"; - const expectedIdTwo = "cbea34a8-bde4-46ad-9d19-b05001228cd3"; + it("should get unsecured ciphers regardless of edit access", async () => { jest.spyOn(component as any, "getAllCiphers").mockReturnValue(Promise.resolve(cipherData)); await component.setCiphers(); + const cipherIds = component.ciphers.map((c) => c.id); + expect(cipherIds).toContain("cbea34a8-bde4-46ad-9d19-b05001228ab2"); + expect(cipherIds).toContain("cbea34a8-bde4-46ad-9d19-b05001228cd3"); expect(component.ciphers.length).toEqual(2); - expect(component.ciphers[0].id).toEqual(expectedIdOne); - expect(component.ciphers[0].edit).toEqual(true); - expect(component.ciphers[1].id).toEqual(expectedIdTwo); - expect(component.ciphers[1].edit).toEqual(true); }); it("should call fullSync method of syncService", () => { diff --git a/apps/web/src/app/dirt/reports/pages/unsecured-websites-report.component.ts b/apps/web/src/app/dirt/reports/pages/unsecured-websites-report.component.ts index 4a2c0677574..8399395d273 100644 --- a/apps/web/src/app/dirt/reports/pages/unsecured-websites-report.component.ts +++ b/apps/web/src/app/dirt/reports/pages/unsecured-websites-report.component.ts @@ -71,12 +71,7 @@ export class UnsecuredWebsitesReportComponent extends CipherReportComponent impl * @param cipher Current cipher with unsecured uri */ private cipherContainsUnsecured(cipher: CipherView): boolean { - if ( - cipher.type !== CipherType.Login || - !cipher.login.hasUris || - cipher.isDeleted || - (!this.organization && !cipher.edit) - ) { + if (cipher.type !== CipherType.Login || !cipher.login.hasUris || cipher.isDeleted) { return false; } diff --git a/apps/web/src/app/dirt/reports/pages/weak-passwords-report.component.spec.ts b/apps/web/src/app/dirt/reports/pages/weak-passwords-report.component.spec.ts index a63723dc688..f9aca0aa378 100644 --- a/apps/web/src/app/dirt/reports/pages/weak-passwords-report.component.spec.ts +++ b/apps/web/src/app/dirt/reports/pages/weak-passwords-report.component.spec.ts @@ -114,10 +114,7 @@ describe("WeakPasswordsReportComponent", () => { expect(component).toBeTruthy(); }); - it('should get only ciphers with weak passwords that the user has "Can Edit" access to', async () => { - const expectedIdOne: any = "cbea34a8-bde4-46ad-9d19-b05001228ab2"; - const expectedIdTwo = "cbea34a8-bde4-46ad-9d19-b05001228cd3"; - + it("should get ciphers with weak passwords regardless of edit access", async () => { jest.spyOn(passwordStrengthService, "getPasswordStrength").mockReturnValue({ password: "123", score: 0, @@ -125,11 +122,11 @@ describe("WeakPasswordsReportComponent", () => { jest.spyOn(component as any, "getAllCiphers").mockReturnValue(Promise.resolve(cipherData)); await component.setCiphers(); - expect(component.ciphers.length).toEqual(2); - expect(component.ciphers[0].id).toEqual(expectedIdOne); - expect(component.ciphers[0].edit).toEqual(true); - expect(component.ciphers[1].id).toEqual(expectedIdTwo); - expect(component.ciphers[1].edit).toEqual(true); + const cipherIds = component.ciphers.map((c) => c.id); + expect(cipherIds).toContain("cbea34a8-bde4-46ad-9d19-b05001228ab1"); + expect(cipherIds).toContain("cbea34a8-bde4-46ad-9d19-b05001228ab2"); + expect(cipherIds).toContain("cbea34a8-bde4-46ad-9d19-b05001228cd3"); + expect(component.ciphers.length).toEqual(3); }); it("should call fullSync method of syncService", () => { diff --git a/apps/web/src/app/dirt/reports/pages/weak-passwords-report.component.ts b/apps/web/src/app/dirt/reports/pages/weak-passwords-report.component.ts index bb5400346fd..6cde01f2d92 100644 --- a/apps/web/src/app/dirt/reports/pages/weak-passwords-report.component.ts +++ b/apps/web/src/app/dirt/reports/pages/weak-passwords-report.component.ts @@ -103,15 +103,8 @@ export class WeakPasswordsReportComponent extends CipherReportComponent implemen } protected determineWeakPasswordScore(ciph: CipherView): ReportResult | null { - const { type, login, isDeleted, edit, viewPassword } = ciph; - if ( - type !== CipherType.Login || - login.password == null || - login.password === "" || - isDeleted || - (!this.organization && !edit) || - !viewPassword - ) { + const { type, login, isDeleted } = ciph; + if (type !== CipherType.Login || login.password == null || login.password === "" || isDeleted) { return; } From bc6b1c3b831778b64e75988e104dbec361113a4c Mon Sep 17 00:00:00 2001 From: John Harrington <84741727+harr1424@users.noreply.github.com> Date: Wed, 18 Feb 2026 09:34:57 -0700 Subject: [PATCH 12/89] [PM-32242] Error message is incorrectly formatted for password protected Send (#18991) * re-work error display to match design specs * fix password auth in attemptV1Access * fix locales file (formatting) --- .../send/send-access/send-auth.component.ts | 27 ++++++++++++++++--- apps/web/src/locales/en/messages.json | 8 ++++-- 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/apps/web/src/app/tools/send/send-access/send-auth.component.ts b/apps/web/src/app/tools/send/send-access/send-auth.component.ts index 8c630ce5315..92c3d445333 100644 --- a/apps/web/src/app/tools/send/send-access/send-auth.component.ts +++ b/apps/web/src/app/tools/send/send-access/send-auth.component.ts @@ -104,7 +104,27 @@ export class SendAuthComponent implements OnInit { } catch (e) { if (e instanceof ErrorResponse) { if (e.statusCode === 401) { + if (this.sendAuthType() === AuthType.Password) { + // Password was already required, so this is an invalid password error + const passwordControl = this.sendAccessForm.get("password"); + if (passwordControl) { + passwordControl.setErrors({ + invalidPassword: { message: this.i18nService.t("sendPasswordInvalidAskOwner") }, + }); + passwordControl.markAsTouched(); + } + } + // Set auth type to Password (either first time or refresh) this.sendAuthType.set(AuthType.Password); + } else if (e.statusCode === 400 && this.sendAuthType() === AuthType.Password) { + // Server returns 400 for SendAccessResult.PasswordInvalid + const passwordControl = this.sendAccessForm.get("password"); + if (passwordControl) { + passwordControl.setErrors({ + invalidPassword: { message: this.i18nService.t("sendPasswordInvalidAskOwner") }, + }); + passwordControl.markAsTouched(); + } } else if (e.statusCode === 404) { this.unavailable.set(true); } else { @@ -175,11 +195,10 @@ export class SendAuthComponent implements OnInit { this.sendAuthType.set(AuthType.Password); this.updatePageTitle(); } else if (passwordHashB64Invalid(response.error)) { - this.toastService.showToast({ - variant: "error", - title: this.i18nService.t("errorOccurred"), - message: this.i18nService.t("invalidSendPassword"), + this.sendAccessForm.controls.password?.setErrors({ + invalidPassword: { message: this.i18nService.t("sendPasswordInvalidAskOwner") }, }); + this.sendAccessForm.controls.password?.markAsTouched(); } else if (sendIdInvalid(response.error)) { this.unavailable.set(true); } else { diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index cc73a04b81b..4731be36ef5 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -12946,8 +12946,13 @@ "paymentMethodUpdateError": { "message": "There was an error updating your payment method." }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "sendExpiresOn": { "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", "placeholders": { "time": { "content": "$1", @@ -12957,7 +12962,6 @@ "content": "$2", "example": "Jan 1, 1970" } - }, - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + } } } From f7f06267ee22e7ba06fc8ef02c8c854810529ada Mon Sep 17 00:00:00 2001 From: Mike Amirault Date: Wed, 18 Feb 2026 11:50:52 -0500 Subject: [PATCH 13/89] [PM-31347] Add missing messages resulting in empty toast on invalid export master password (#19037) --- apps/browser/src/_locales/en/messages.json | 3 +++ apps/desktop/src/locales/en/messages.json | 3 +++ 2 files changed, 6 insertions(+) diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index a221dc4f338..5ed97ce0f07 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -6173,5 +6173,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index f444265877d..85ef3d94001 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -4617,5 +4617,8 @@ }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } From 935bf3655cd8ac0c62eff3956d116df0739044b6 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Wed, 18 Feb 2026 18:08:16 +0100 Subject: [PATCH 14/89] Update sdk to 546 (#19056) --- package-lock.json | 16 ++++++++-------- package.json | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8d3c32c027d..fb1111a82b9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,8 +23,8 @@ "@angular/platform-browser": "20.3.16", "@angular/platform-browser-dynamic": "20.3.16", "@angular/router": "20.3.16", - "@bitwarden/commercial-sdk-internal": "0.2.0-main.527", - "@bitwarden/sdk-internal": "0.2.0-main.527", + "@bitwarden/commercial-sdk-internal": "0.2.0-main.546", + "@bitwarden/sdk-internal": "0.2.0-main.546", "@electron/fuses": "1.8.0", "@emotion/css": "11.13.5", "@koa/multer": "4.0.0", @@ -4936,9 +4936,9 @@ "link": true }, "node_modules/@bitwarden/commercial-sdk-internal": { - "version": "0.2.0-main.527", - "resolved": "https://registry.npmjs.org/@bitwarden/commercial-sdk-internal/-/commercial-sdk-internal-0.2.0-main.527.tgz", - "integrity": "sha512-4C4lwOgA2v184G2axUR5Jdb4UMXMhF52a/3c0lAZYbD/8Nid6jziE89nCa9hdfdazuPgWXhVFa3gPrhLZ4uTUQ==", + "version": "0.2.0-main.546", + "resolved": "https://registry.npmjs.org/@bitwarden/commercial-sdk-internal/-/commercial-sdk-internal-0.2.0-main.546.tgz", + "integrity": "sha512-3lIQSb1yYSpDqhgT2uqHjPC88yVL7rWR08i0XD0BQJMFfN0FcB378r2Fq6d5TMXLPEYZ8PR62BCDB+tYKM7FPw==", "license": "BITWARDEN SOFTWARE DEVELOPMENT KIT LICENSE AGREEMENT", "dependencies": { "type-fest": "^4.41.0" @@ -5041,9 +5041,9 @@ "link": true }, "node_modules/@bitwarden/sdk-internal": { - "version": "0.2.0-main.527", - "resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.527.tgz", - "integrity": "sha512-dxPh4XjEGFDBASRBEd/JwUdoMAz10W/0QGygYkPwhKKGzJncfDEAgQ/KrT9wc36ycrDrOOspff7xs/vmmzI0+A==", + "version": "0.2.0-main.546", + "resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.546.tgz", + "integrity": "sha512-KGPyP1pr7aIBaJ9Knibpfjydo/27Rlve77X4ENmDIwrSJ9FB3o2B6D3UXpNNVyXKt2Ii1C+rNT7ezMRO25Qs4A==", "license": "GPL-3.0", "dependencies": { "type-fest": "^4.41.0" diff --git a/package.json b/package.json index 7499a69f99c..c18112989fe 100644 --- a/package.json +++ b/package.json @@ -161,8 +161,8 @@ "@angular/platform-browser": "20.3.16", "@angular/platform-browser-dynamic": "20.3.16", "@angular/router": "20.3.16", - "@bitwarden/commercial-sdk-internal": "0.2.0-main.527", - "@bitwarden/sdk-internal": "0.2.0-main.527", + "@bitwarden/commercial-sdk-internal": "0.2.0-main.546", + "@bitwarden/sdk-internal": "0.2.0-main.546", "@electron/fuses": "1.8.0", "@emotion/css": "11.13.5", "@koa/multer": "4.0.0", From c086df14e7c27433eda798a10abad4a25c1635bb Mon Sep 17 00:00:00 2001 From: Todd Martin <106564991+trmartin4@users.noreply.github.com> Date: Wed, 18 Feb 2026 12:56:53 -0500 Subject: [PATCH 15/89] chore(ownership): Move account-fingerprint to KM ownership --- .../organizations/settings/organization-settings.module.ts | 2 +- apps/web/src/app/auth/settings/account/profile.component.ts | 2 +- .../account-fingerprint/account-fingerprint.component.html | 0 .../account-fingerprint/account-fingerprint.component.ts | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) rename apps/web/src/app/{shared/components => key-management}/account-fingerprint/account-fingerprint.component.html (100%) rename apps/web/src/app/{shared/components => key-management}/account-fingerprint/account-fingerprint.component.ts (96%) diff --git a/apps/web/src/app/admin-console/organizations/settings/organization-settings.module.ts b/apps/web/src/app/admin-console/organizations/settings/organization-settings.module.ts index 27a6226f964..13467e222d2 100644 --- a/apps/web/src/app/admin-console/organizations/settings/organization-settings.module.ts +++ b/apps/web/src/app/admin-console/organizations/settings/organization-settings.module.ts @@ -4,9 +4,9 @@ import { PremiumBadgeComponent } from "@bitwarden/angular/billing/components/pre import { ItemModule } from "@bitwarden/components"; import { DangerZoneComponent } from "../../../auth/settings/account/danger-zone.component"; +import { AccountFingerprintComponent } from "../../../key-management/account-fingerprint/account-fingerprint.component"; import { HeaderModule } from "../../../layouts/header/header.module"; import { SharedModule } from "../../../shared"; -import { AccountFingerprintComponent } from "../../../shared/components/account-fingerprint/account-fingerprint.component"; import { AccountComponent } from "./account.component"; import { OrganizationSettingsRoutingModule } from "./organization-settings-routing.module"; diff --git a/apps/web/src/app/auth/settings/account/profile.component.ts b/apps/web/src/app/auth/settings/account/profile.component.ts index fd96f343b3a..24e8a370e2a 100644 --- a/apps/web/src/app/auth/settings/account/profile.component.ts +++ b/apps/web/src/app/auth/settings/account/profile.component.ts @@ -18,8 +18,8 @@ import { DialogService, ToastService } from "@bitwarden/components"; import { KeyService } from "@bitwarden/key-management"; import { DynamicAvatarComponent } from "../../../components/dynamic-avatar.component"; +import { AccountFingerprintComponent } from "../../../key-management/account-fingerprint/account-fingerprint.component"; import { SharedModule } from "../../../shared"; -import { AccountFingerprintComponent } from "../../../shared/components/account-fingerprint/account-fingerprint.component"; import { ChangeAvatarDialogComponent } from "./change-avatar-dialog.component"; diff --git a/apps/web/src/app/shared/components/account-fingerprint/account-fingerprint.component.html b/apps/web/src/app/key-management/account-fingerprint/account-fingerprint.component.html similarity index 100% rename from apps/web/src/app/shared/components/account-fingerprint/account-fingerprint.component.html rename to apps/web/src/app/key-management/account-fingerprint/account-fingerprint.component.html diff --git a/apps/web/src/app/shared/components/account-fingerprint/account-fingerprint.component.ts b/apps/web/src/app/key-management/account-fingerprint/account-fingerprint.component.ts similarity index 96% rename from apps/web/src/app/shared/components/account-fingerprint/account-fingerprint.component.ts rename to apps/web/src/app/key-management/account-fingerprint/account-fingerprint.component.ts index eb84868dca1..ca9042e802e 100644 --- a/apps/web/src/app/shared/components/account-fingerprint/account-fingerprint.component.ts +++ b/apps/web/src/app/key-management/account-fingerprint/account-fingerprint.component.ts @@ -4,7 +4,7 @@ import { Component, Input, OnInit } from "@angular/core"; import { KeyService } from "@bitwarden/key-management"; -import { SharedModule } from "../../shared.module"; +import { SharedModule } from "../../shared/shared.module"; // FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush // eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection From 5444869456317fc77f43d4639155838f0ce890b9 Mon Sep 17 00:00:00 2001 From: Isaac Ivins Date: Wed, 18 Feb 2026 13:20:08 -0500 Subject: [PATCH 16/89] PM-31733: Sends Drawer Persisting On Side Nav Change (#18762) * using activeDrawerRef with onDestroy * improved refs type checking - removed cdr --- .../app/tools/send-v2/send-v2.component.ts | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/apps/desktop/src/app/tools/send-v2/send-v2.component.ts b/apps/desktop/src/app/tools/send-v2/send-v2.component.ts index 271418ae5b2..fc058c1a17f 100644 --- a/apps/desktop/src/app/tools/send-v2/send-v2.component.ts +++ b/apps/desktop/src/app/tools/send-v2/send-v2.component.ts @@ -1,6 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { Component, computed, inject, signal, viewChild } from "@angular/core"; +import { Component, computed, DestroyRef, inject, signal, viewChild } from "@angular/core"; import { toSignal } from "@angular/core/rxjs-interop"; import { combineLatest, map, switchMap, lastValueFrom } from "rxjs"; @@ -20,7 +20,7 @@ import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.s import { SendType } from "@bitwarden/common/tools/send/types/send-type"; import { SendId } from "@bitwarden/common/types/guid"; import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service"; -import { ButtonModule, DialogService, ToastService } from "@bitwarden/components"; +import { ButtonModule, DialogRef, DialogService, ToastService } from "@bitwarden/components"; import { NewSendDropdownV2Component, SendItemsService, @@ -28,6 +28,7 @@ import { SendListState, SendAddEditDialogComponent, DefaultSendFormConfigService, + SendItemDialogResult, } from "@bitwarden/send-ui"; import { DesktopPremiumUpgradePromptService } from "../../../services/desktop-premium-upgrade-prompt.service"; @@ -84,6 +85,9 @@ export class SendV2Component { private dialogService = inject(DialogService); private toastService = inject(ToastService); private logService = inject(LogService); + private destroyRef = inject(DestroyRef); + + private activeDrawerRef?: DialogRef; protected readonly useDrawerEditMode = toSignal( this.configService.getFeatureFlag$(FeatureFlag.DesktopUiMigrationMilestone2), @@ -128,6 +132,12 @@ export class SendV2Component { { initialValue: null }, ); + constructor() { + this.destroyRef.onDestroy(() => { + this.activeDrawerRef?.close(); + }); + } + protected readonly selectedSendType = computed(() => { const action = this.action(); @@ -143,11 +153,12 @@ export class SendV2Component { if (this.useDrawerEditMode()) { const formConfig = await this.sendFormConfigService.buildConfig("add", undefined, type); - const dialogRef = SendAddEditDialogComponent.openDrawer(this.dialogService, { + this.activeDrawerRef = SendAddEditDialogComponent.openDrawer(this.dialogService, { formConfig, }); - await lastValueFrom(dialogRef.closed); + await lastValueFrom(this.activeDrawerRef.closed); + this.activeDrawerRef = null; } else { this.action.set(Action.Add); this.sendId.set(null); @@ -173,11 +184,12 @@ export class SendV2Component { if (this.useDrawerEditMode()) { const formConfig = await this.sendFormConfigService.buildConfig("edit", sendId as SendId); - const dialogRef = SendAddEditDialogComponent.openDrawer(this.dialogService, { + this.activeDrawerRef = SendAddEditDialogComponent.openDrawer(this.dialogService, { formConfig, }); - await lastValueFrom(dialogRef.closed); + await lastValueFrom(this.activeDrawerRef.closed); + this.activeDrawerRef = null; } else { if (sendId === this.sendId() && this.action() === Action.Edit) { return; From ab595900196800944cdc6fa8f286eaa03e2038a0 Mon Sep 17 00:00:00 2001 From: Stephon Brown Date: Wed, 18 Feb 2026 14:32:08 -0500 Subject: [PATCH 17/89] [PM-29823] Add Tests for Updates (#19040) * refactor: Remove direct self-hosted org creation from OrganizationPlansComponent * tests: Add comprehensive test suite for OrganizationPlansComponent --- .../organization-plans.component.spec.ts | 2199 +++++++++++++++++ .../organization-plans.component.ts | 27 +- 2 files changed, 2200 insertions(+), 26 deletions(-) create mode 100644 apps/web/src/app/billing/organizations/organization-plans.component.spec.ts diff --git a/apps/web/src/app/billing/organizations/organization-plans.component.spec.ts b/apps/web/src/app/billing/organizations/organization-plans.component.spec.ts new file mode 100644 index 00000000000..aa4cbdab40e --- /dev/null +++ b/apps/web/src/app/billing/organizations/organization-plans.component.spec.ts @@ -0,0 +1,2199 @@ +// These are disabled until we can migrate to signals and remove the use of @Input properties that are used within the mocked child components +/* eslint-disable @angular-eslint/prefer-output-emitter-ref */ +/* eslint-disable @angular-eslint/prefer-signals */ +import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from "@angular/core"; +import { ComponentFixture, fakeAsync, TestBed, tick } from "@angular/core/testing"; +import { FormBuilder, Validators } from "@angular/forms"; +import { Router } from "@angular/router"; +import { BehaviorSubject, of } from "rxjs"; + +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; +import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { PlanType, ProductTierType } from "@bitwarden/common/billing/enums"; +import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; +import { ToastService } from "@bitwarden/components"; +import { KeyService } from "@bitwarden/key-management"; +import { + PreviewInvoiceClient, + SubscriberBillingClient, +} from "@bitwarden/web-vault/app/billing/clients"; + +import { OrganizationInformationComponent } from "../../admin-console/organizations/create/organization-information.component"; +import { EnterBillingAddressComponent, EnterPaymentMethodComponent } from "../payment/components"; +import { SecretsManagerSubscribeComponent } from "../shared"; +import { OrganizationSelfHostingLicenseUploaderComponent } from "../shared/self-hosting-license-uploader/organization-self-hosting-license-uploader.component"; + +import { OrganizationPlansComponent } from "./organization-plans.component"; + +// Mocked Child Components +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + selector: "app-org-info", + template: "", + standalone: true, +}) +class MockOrgInfoComponent { + @Input() formGroup: any; + @Input() createOrganization = true; + @Input() isProvider = false; + @Input() acceptingSponsorship = false; + @Output() changedBusinessOwned = new EventEmitter(); +} + +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + selector: "sm-subscribe", + template: "", + standalone: true, +}) +class MockSmSubscribeComponent { + @Input() formGroup: any; + @Input() selectedPlan: any; + @Input() upgradeOrganization = false; +} + +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + selector: "app-enter-payment-method", + template: "", + standalone: true, +}) +class MockEnterPaymentMethodComponent { + @Input() group: any; + + static getFormGroup() { + const fb = new FormBuilder(); + return fb.group({ + type: fb.control("card"), + bankAccount: fb.group({ + routingNumber: fb.control(""), + accountNumber: fb.control(""), + accountHolderName: fb.control(""), + accountHolderType: fb.control(""), + }), + billingAddress: fb.group({ + country: fb.control("US"), + postalCode: fb.control(""), + }), + }); + } + + tokenize = jest.fn().mockResolvedValue({ + token: "mock_token", + type: "card", + }); +} + +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + selector: "app-enter-billing-address", + template: "", + standalone: true, +}) +class MockEnterBillingAddressComponent { + @Input() group: any; + @Input() scenario: any; + + static getFormGroup() { + return new FormBuilder().group({ + country: ["US", Validators.required], + postalCode: ["", Validators.required], + taxId: [""], + line1: [""], + line2: [""], + city: [""], + state: [""], + }); + } +} + +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + selector: "organization-self-hosting-license-uploader", + template: "", + standalone: true, +}) +class MockOrganizationSelfHostingLicenseUploaderComponent { + @Output() onLicenseFileUploaded = new EventEmitter(); +} + +// Test Helper Functions + +/** + * Sets up mock encryption keys and org key services + */ +const setupMockEncryptionKeys = ( + mockKeyService: jest.Mocked, + mockEncryptService: jest.Mocked, +) => { + mockKeyService.makeOrgKey.mockResolvedValue([{ encryptedString: "mock-key" }, {} as any] as any); + + mockEncryptService.encryptString.mockResolvedValue({ + encryptedString: "mock-collection", + } as any); + + mockKeyService.makeKeyPair.mockResolvedValue([ + "public-key", + { encryptedString: "private-key" }, + ] as any); +}; + +/** + * Sets up a mock payment method component that returns a successful tokenization + */ +const setupMockPaymentMethodComponent = ( + component: OrganizationPlansComponent, + token = "mock_token", + type = "card", +) => { + component["enterPaymentMethodComponent"] = { + tokenize: jest.fn().mockResolvedValue({ token, type }), + } as any; +}; + +/** + * Patches billing address form with standard test values + */ +const patchBillingAddress = ( + component: OrganizationPlansComponent, + overrides: Partial<{ + country: string; + postalCode: string; + line1: string; + line2: string; + city: string; + state: string; + taxId: string; + }> = {}, +) => { + component.billingFormGroup.controls.billingAddress.patchValue({ + country: "US", + postalCode: "12345", + line1: "123 Street", + line2: "", + city: "City", + state: "CA", + taxId: "", + ...overrides, + }); +}; + +/** + * Sets up a mock organization for upgrade scenarios + */ +const setupMockUpgradeOrganization = ( + mockOrganizationApiService: jest.Mocked, + organizationsSubject: BehaviorSubject, + orgConfig: { + id?: string; + productTierType?: ProductTierType; + hasPaymentSource?: boolean; + planType?: PlanType; + seats?: number; + maxStorageGb?: number; + hasPublicAndPrivateKeys?: boolean; + useSecretsManager?: boolean; + smSeats?: number; + smServiceAccounts?: number; + } = {}, +) => { + const { + id = "org-123", + productTierType = ProductTierType.Free, + hasPaymentSource = true, + planType = PlanType.Free, + seats = 5, + maxStorageGb, + hasPublicAndPrivateKeys = true, + useSecretsManager = false, + smSeats, + smServiceAccounts, + } = orgConfig; + + const mockOrganization = { + id, + name: "Test Org", + productTierType, + seats, + maxStorageGb, + hasPublicAndPrivateKeys, + useSecretsManager, + } as Organization; + + organizationsSubject.next([mockOrganization]); + + mockOrganizationApiService.getBilling.mockResolvedValue({ + paymentSource: hasPaymentSource ? { type: "card" } : null, + } as any); + + mockOrganizationApiService.getSubscription.mockResolvedValue({ + planType, + smSeats, + smServiceAccounts, + } as any); + + return mockOrganization; +}; + +/** + * Patches organization form with basic test values + */ +const patchOrganizationForm = ( + component: OrganizationPlansComponent, + values: { + name?: string; + billingEmail?: string; + productTier?: ProductTierType; + plan?: PlanType; + additionalSeats?: number; + additionalStorage?: number; + }, +) => { + component.formGroup.patchValue({ + name: "Test Org", + billingEmail: "test@example.com", + productTier: ProductTierType.Free, + plan: PlanType.Free, + additionalSeats: 0, + additionalStorage: 0, + ...values, + }); +}; + +/** + * Returns plan details + * + */ + +const createMockPlans = (): PlanResponse[] => { + return [ + { + type: PlanType.Free, + productTier: ProductTierType.Free, + name: "Free", + isAnnual: true, + upgradeSortOrder: 1, + displaySortOrder: 1, + PasswordManager: { + basePrice: 0, + seatPrice: 0, + maxSeats: 2, + baseSeats: 2, + hasAdditionalSeatsOption: false, + hasAdditionalStorageOption: false, + hasPremiumAccessOption: false, + baseStorageGb: 0, + }, + SecretsManager: null, + } as PlanResponse, + { + type: PlanType.FamiliesAnnually, + productTier: ProductTierType.Families, + name: "Families", + isAnnual: true, + upgradeSortOrder: 2, + displaySortOrder: 2, + PasswordManager: { + basePrice: 40, + seatPrice: 0, + maxSeats: 6, + baseSeats: 6, + hasAdditionalSeatsOption: false, + hasAdditionalStorageOption: true, + hasPremiumAccessOption: false, + baseStorageGb: 1, + additionalStoragePricePerGb: 4, + }, + SecretsManager: null, + } as PlanResponse, + { + type: PlanType.TeamsAnnually, + productTier: ProductTierType.Teams, + name: "Teams", + isAnnual: true, + canBeUsedByBusiness: true, + upgradeSortOrder: 3, + displaySortOrder: 3, + PasswordManager: { + basePrice: 0, + seatPrice: 48, + hasAdditionalSeatsOption: true, + hasAdditionalStorageOption: true, + hasPremiumAccessOption: true, + baseStorageGb: 1, + additionalStoragePricePerGb: 4, + premiumAccessOptionPrice: 40, + }, + SecretsManager: { + basePrice: 0, + seatPrice: 72, + hasAdditionalSeatsOption: true, + hasAdditionalServiceAccountOption: true, + baseServiceAccount: 50, + additionalPricePerServiceAccount: 6, + }, + } as PlanResponse, + { + type: PlanType.EnterpriseAnnually, + productTier: ProductTierType.Enterprise, + name: "Enterprise", + isAnnual: true, + canBeUsedByBusiness: true, + trialPeriodDays: 7, + upgradeSortOrder: 4, + displaySortOrder: 4, + PasswordManager: { + basePrice: 0, + seatPrice: 72, + hasAdditionalSeatsOption: true, + hasAdditionalStorageOption: true, + hasPremiumAccessOption: true, + baseStorageGb: 1, + additionalStoragePricePerGb: 4, + premiumAccessOptionPrice: 40, + }, + SecretsManager: { + basePrice: 0, + seatPrice: 144, + hasAdditionalSeatsOption: true, + hasAdditionalServiceAccountOption: true, + baseServiceAccount: 200, + additionalPricePerServiceAccount: 6, + }, + } as PlanResponse, + ]; +}; + +describe("OrganizationPlansComponent", () => { + let component: OrganizationPlansComponent; + let fixture: ComponentFixture; + + // Mock services + let mockApiService: jest.Mocked; + let mockI18nService: jest.Mocked; + let mockPlatformUtilsService: jest.Mocked; + let mockKeyService: jest.Mocked; + let mockEncryptService: jest.Mocked; + let mockRouter: jest.Mocked; + let mockSyncService: jest.Mocked; + let mockPolicyService: jest.Mocked; + let mockOrganizationService: jest.Mocked; + let mockMessagingService: jest.Mocked; + let mockOrganizationApiService: jest.Mocked; + let mockProviderApiService: jest.Mocked; + let mockToastService: jest.Mocked; + let mockAccountService: jest.Mocked; + let mockSubscriberBillingClient: jest.Mocked; + let mockPreviewInvoiceClient: jest.Mocked; + let mockConfigService: jest.Mocked; + + // Mock data + let mockPasswordManagerPlans: PlanResponse[]; + let mockOrganization: Organization; + let activeAccountSubject: BehaviorSubject; + let organizationsSubject: BehaviorSubject; + + beforeEach(async () => { + jest.clearAllMocks(); + + // Mock the static getFormGroup methods to return forms without validators + jest + .spyOn(EnterPaymentMethodComponent, "getFormGroup") + .mockReturnValue(MockEnterPaymentMethodComponent.getFormGroup() as any); + jest + .spyOn(EnterBillingAddressComponent, "getFormGroup") + .mockReturnValue(MockEnterBillingAddressComponent.getFormGroup() as any); + + // Initialize mock services + mockApiService = { + getPlans: jest.fn(), + postProviderCreateOrganization: jest.fn(), + refreshIdentityToken: jest.fn(), + } as any; + + mockI18nService = { + t: jest.fn((key: string) => key), + } as any; + + mockPlatformUtilsService = { + isSelfHost: jest.fn().mockReturnValue(false), + } as any; + + mockKeyService = { + makeOrgKey: jest.fn(), + makeKeyPair: jest.fn(), + orgKeys$: jest.fn().mockReturnValue(of({})), + providerKeys$: jest.fn().mockReturnValue(of({})), + } as any; + + mockEncryptService = { + encryptString: jest.fn(), + wrapSymmetricKey: jest.fn(), + } as any; + + mockRouter = { + navigate: jest.fn(), + } as any; + + mockSyncService = { + fullSync: jest.fn().mockResolvedValue(undefined), + } as any; + + mockPolicyService = { + policyAppliesToUser$: jest.fn().mockReturnValue(of(false)), + } as any; + + // Setup subjects for observables + activeAccountSubject = new BehaviorSubject({ + id: "user-id", + email: "test@example.com", + }); + organizationsSubject = new BehaviorSubject([]); + + mockAccountService = { + activeAccount$: activeAccountSubject.asObservable(), + } as any; + + mockOrganizationService = { + organizations$: jest.fn().mockReturnValue(organizationsSubject.asObservable()), + } as any; + + mockMessagingService = { + send: jest.fn(), + } as any; + + mockOrganizationApiService = { + getBilling: jest.fn(), + getSubscription: jest.fn(), + create: jest.fn(), + createLicense: jest.fn(), + upgrade: jest.fn(), + updateKeys: jest.fn(), + } as any; + + mockProviderApiService = { + getProvider: jest.fn(), + } as any; + + mockToastService = { + showToast: jest.fn(), + } as any; + + mockSubscriberBillingClient = { + getBillingAddress: jest.fn().mockResolvedValue({ + country: "US", + postalCode: "12345", + }), + updatePaymentMethod: jest.fn().mockResolvedValue(undefined), + } as any; + + mockPreviewInvoiceClient = { + previewTaxForOrganizationSubscriptionPurchase: jest.fn().mockResolvedValue({ + tax: 5.0, + total: 50.0, + }), + } as any; + + mockConfigService = { + getFeatureFlag: jest.fn().mockResolvedValue(true), + } as any; + + // Setup mock plan data + mockPasswordManagerPlans = createMockPlans(); + + mockApiService.getPlans.mockResolvedValue({ + data: mockPasswordManagerPlans, + } as any); + + await TestBed.configureTestingModule({ + providers: [ + { provide: ApiService, useValue: mockApiService }, + { provide: I18nService, useValue: mockI18nService }, + { provide: PlatformUtilsService, useValue: mockPlatformUtilsService }, + { provide: KeyService, useValue: mockKeyService }, + { provide: EncryptService, useValue: mockEncryptService }, + { provide: Router, useValue: mockRouter }, + { provide: SyncService, useValue: mockSyncService }, + { provide: PolicyService, useValue: mockPolicyService }, + { provide: OrganizationService, useValue: mockOrganizationService }, + { provide: MessagingService, useValue: mockMessagingService }, + FormBuilder, // Use real FormBuilder + { provide: OrganizationApiServiceAbstraction, useValue: mockOrganizationApiService }, + { provide: ProviderApiServiceAbstraction, useValue: mockProviderApiService }, + { provide: ToastService, useValue: mockToastService }, + { provide: AccountService, useValue: mockAccountService }, + { provide: SubscriberBillingClient, useValue: mockSubscriberBillingClient }, + { provide: PreviewInvoiceClient, useValue: mockPreviewInvoiceClient }, + { provide: ConfigService, useValue: mockConfigService }, + ], + }) + // Override the component to replace child components with mocks and provide mock services + .overrideComponent(OrganizationPlansComponent, { + remove: { + imports: [ + OrganizationInformationComponent, + SecretsManagerSubscribeComponent, + EnterPaymentMethodComponent, + EnterBillingAddressComponent, + OrganizationSelfHostingLicenseUploaderComponent, + ], + providers: [PreviewInvoiceClient, SubscriberBillingClient], + }, + add: { + imports: [ + MockOrgInfoComponent, + MockSmSubscribeComponent, + MockEnterPaymentMethodComponent, + MockEnterBillingAddressComponent, + MockOrganizationSelfHostingLicenseUploaderComponent, + ], + providers: [ + { provide: PreviewInvoiceClient, useValue: mockPreviewInvoiceClient }, + { provide: SubscriberBillingClient, useValue: mockSubscriberBillingClient }, + ], + }, + }) + .compileComponents(); + + fixture = TestBed.createComponent(OrganizationPlansComponent); + component = fixture.componentInstance; + }); + + describe("component creation", () => { + it("should create", () => { + expect(component).toBeTruthy(); + }); + + it("should initialize with default values", () => { + expect(component.loading).toBe(true); + expect(component.showFree).toBe(true); + expect(component.showCancel).toBe(false); + expect(component.productTier).toBe(ProductTierType.Free); + }); + }); + + describe("ngOnInit", () => { + describe("create organization flow", () => { + it("should load plans from API", async () => { + fixture.detectChanges(); + await fixture.whenStable(); + + expect(mockApiService.getPlans).toHaveBeenCalled(); + expect(component.passwordManagerPlans).toEqual(mockPasswordManagerPlans); + expect(component.loading).toBe(false); + }); + + it("should set required validators on name and billing email", async () => { + fixture.detectChanges(); + await fixture.whenStable(); + + component.formGroup.controls.name.setValue(""); + component.formGroup.controls.billingEmail.setValue(""); + + expect(component.formGroup.controls.name.hasError("required")).toBe(true); + expect(component.formGroup.controls.billingEmail.hasError("required")).toBe(true); + }); + + it("should not load organization data for create flow", async () => { + fixture.detectChanges(); + await fixture.whenStable(); + + expect(mockOrganizationApiService.getBilling).not.toHaveBeenCalled(); + expect(mockOrganizationApiService.getSubscription).not.toHaveBeenCalled(); + }); + }); + + describe("upgrade organization flow", () => { + beforeEach(() => { + mockOrganization = setupMockUpgradeOrganization( + mockOrganizationApiService, + organizationsSubject, + { + planType: PlanType.FamiliesAnnually2025, + }, + ); + + component.organizationId = mockOrganization.id; + }); + + it("should load existing organization data", async () => { + fixture.detectChanges(); + await fixture.whenStable(); + + expect(component.organization).toEqual(mockOrganization); + expect(mockOrganizationApiService.getBilling).toHaveBeenCalledWith(mockOrganization.id); + expect(mockOrganizationApiService.getSubscription).toHaveBeenCalledWith( + mockOrganization.id, + ); + expect(mockSubscriberBillingClient.getBillingAddress).toHaveBeenCalledWith({ + type: "organization", + data: mockOrganization, + }); + // Verify the form was updated + expect(component.billingFormGroup.controls.billingAddress.value.country).toBe("US"); + expect(component.billingFormGroup.controls.billingAddress.value.postalCode).toBe("12345"); + }); + + it("should not add validators for name and billingEmail in upgrade flow", async () => { + fixture.detectChanges(); + await fixture.whenStable(); + + component.formGroup.controls.name.setValue(""); + component.formGroup.controls.billingEmail.setValue(""); + + // In upgrade flow, these should not be required + expect(component.formGroup.controls.name.hasError("required")).toBe(false); + expect(component.formGroup.controls.billingEmail.hasError("required")).toBe(false); + }); + }); + + describe("feature flags", () => { + it("should use FamiliesAnnually when PM26462_Milestone_3 is enabled", async () => { + mockConfigService.getFeatureFlag.mockResolvedValue(true); + + fixture.detectChanges(); + await fixture.whenStable(); + + const familyPlan = component["_familyPlan"]; + expect(familyPlan).toBe(PlanType.FamiliesAnnually); + }); + + it("should use FamiliesAnnually2025 when feature flag is disabled", async () => { + mockConfigService.getFeatureFlag.mockResolvedValue(false); + + fixture.detectChanges(); + await fixture.whenStable(); + + const familyPlan = component["_familyPlan"]; + expect(familyPlan).toBe(PlanType.FamiliesAnnually2025); + }); + }); + }); + + describe("organization creation validation flow", () => { + beforeEach(async () => { + fixture.detectChanges(); + await fixture.whenStable(); + }); + + it("should prevent submission with invalid form data", async () => { + component.formGroup.patchValue({ + name: "", + billingEmail: "invalid-email", + additionalStorage: -1, + additionalSeats: 200000, + }); + + await component.submit(); + + expect(mockOrganizationApiService.create).not.toHaveBeenCalled(); + expect(component.formGroup.invalid).toBe(true); + }); + + it("should allow submission with valid form data", async () => { + patchOrganizationForm(component, { + name: "Valid Organization", + billingEmail: "valid@example.com", + productTier: ProductTierType.Free, + plan: PlanType.Free, + }); + + setupMockEncryptionKeys(mockKeyService, mockEncryptService); + mockOrganizationApiService.create.mockResolvedValue({ + id: "new-org-id", + } as any); + + await component.submit(); + + expect(mockOrganizationApiService.create).toHaveBeenCalled(); + }); + }); + + describe("plan selection flow", () => { + beforeEach(async () => { + fixture.detectChanges(); + await fixture.whenStable(); + }); + + it("should configure form appropriately when switching between product tiers", () => { + // Start with Families plan with unsupported features + component.productTier = ProductTierType.Families; + component.formGroup.controls.additionalSeats.setValue(10); + component.formGroup.controls.additionalStorage.setValue(5); + component.changedProduct(); + + // Families doesn't support additional seats + expect(component.formGroup.controls.additionalSeats.value).toBe(0); + expect(component.formGroup.controls.plan.value).toBe(PlanType.FamiliesAnnually); + + // Switch to Teams plan which supports additional seats + component.productTier = ProductTierType.Teams; + component.changedProduct(); + + expect(component.formGroup.controls.plan.value).toBe(PlanType.TeamsAnnually); + // Teams initializes with 1 seat by default + expect(component.formGroup.controls.additionalSeats.value).toBeGreaterThan(0); + + // Switch to Free plan which doesn't support additional storage + component.formGroup.controls.additionalStorage.setValue(10); + component.productTier = ProductTierType.Free; + component.changedProduct(); + + expect(component.formGroup.controls.additionalStorage.value).toBe(0); + }); + }); + + describe("subscription pricing flow", () => { + beforeEach(async () => { + fixture.detectChanges(); + await fixture.whenStable(); + }); + + it("should calculate total price based on selected plan options", () => { + // Select Teams plan and configure options + component.productTier = ProductTierType.Teams; + component.changedProduct(); + component.formGroup.controls.additionalSeats.setValue(5); + component.formGroup.controls.additionalStorage.setValue(10); + component.formGroup.controls.premiumAccessAddon.setValue(true); + + const pmSubtotal = component.passwordManagerSubtotal; + // Verify pricing includes all selected options + expect(pmSubtotal).toBeGreaterThan(0); + expect(pmSubtotal).toBe(5 * 48 + 10 * 4 + 40); // seats + storage + premium + }); + + it("should calculate pricing with Secrets Manager addon", () => { + component.productTier = ProductTierType.Teams; + component.plan = PlanType.TeamsAnnually; + + // Enable Secrets Manager with additional options + component.secretsManagerForm.patchValue({ + enabled: true, + userSeats: 3, + additionalServiceAccounts: 10, + }); + + const smSubtotal = component.secretsManagerSubtotal; + expect(smSubtotal).toBeGreaterThan(0); + + // Disable Secrets Manager + component.secretsManagerForm.patchValue({ + enabled: false, + }); + + expect(component.secretsManagerSubtotal).toBe(0); + }); + }); + + describe("tax calculation", () => { + beforeEach(async () => { + fixture.detectChanges(); + await fixture.whenStable(); + }); + + it("should calculate tax after debounce period", fakeAsync(() => { + component.productTier = ProductTierType.Teams; + component.changedProduct(); + component.formGroup.controls.additionalSeats.setValue(1); + component.billingFormGroup.controls.billingAddress.patchValue({ + country: "US", + postalCode: "12345", + }); + + tick(1500); // Wait for debounce (1000ms) + + expect( + mockPreviewInvoiceClient.previewTaxForOrganizationSubscriptionPurchase, + ).toHaveBeenCalled(); + expect(component["estimatedTax"]).toBe(5.0); + })); + + it("should not calculate tax with invalid billing address", fakeAsync(() => { + component.billingFormGroup.controls.billingAddress.patchValue({ + country: "", + postalCode: "", + }); + + tick(1500); + + expect( + mockPreviewInvoiceClient.previewTaxForOrganizationSubscriptionPurchase, + ).not.toHaveBeenCalled(); + })); + }); + + describe("submit", () => { + beforeEach(async () => { + fixture.detectChanges(); + await fixture.whenStable(); + }); + + it("should create organization successfully", async () => { + patchOrganizationForm(component, { + name: "New Org", + billingEmail: "test@example.com", + }); + + setupMockEncryptionKeys(mockKeyService, mockEncryptService); + mockOrganizationApiService.create.mockResolvedValue({ + id: "new-org-id", + } as any); + + await component.submit(); + + expect(mockOrganizationApiService.create).toHaveBeenCalled(); + expect(mockToastService.showToast).toHaveBeenCalledWith({ + variant: "success", + title: "organizationCreated", + message: "organizationReadyToGo", + }); + expect(mockSyncService.fullSync).toHaveBeenCalledWith(true); + }); + + it("should emit onSuccess after successful creation", async () => { + const onSuccessSpy = jest.spyOn(component.onSuccess, "emit"); + + patchOrganizationForm(component, { + name: "New Org", + billingEmail: "test@example.com", + }); + + setupMockEncryptionKeys(mockKeyService, mockEncryptService); + mockOrganizationApiService.create.mockResolvedValue({ + id: "new-org-id", + } as any); + + await component.submit(); + + expect(onSuccessSpy).toHaveBeenCalledWith({ + organizationId: "new-org-id", + }); + }); + + it("should handle payment method validation failure", async () => { + patchOrganizationForm(component, { + name: "New Org", + billingEmail: "test@example.com", + productTier: ProductTierType.Teams, + plan: PlanType.TeamsAnnually, + additionalSeats: 5, + }); + + patchBillingAddress(component); + setupMockEncryptionKeys(mockKeyService, mockEncryptService); + + // Mock payment method component to return null (failure) + component["enterPaymentMethodComponent"] = { + tokenize: jest.fn().mockResolvedValue(null), + } as any; + + await component.submit(); + + // Should not create organization if payment method validation fails + expect(mockOrganizationApiService.create).not.toHaveBeenCalled(); + }); + + it("should block submission when single org policy applies", async () => { + mockPolicyService.policyAppliesToUser$.mockReturnValue(of(true)); + + // Need to reinitialize after changing policy mock + const policyFixture = TestBed.createComponent(OrganizationPlansComponent); + const policyComponent = policyFixture.componentInstance; + policyFixture.detectChanges(); + await policyFixture.whenStable(); + + policyComponent.formGroup.patchValue({ + name: "Test", + billingEmail: "test@example.com", + }); + + await policyComponent.submit(); + + expect(mockOrganizationApiService.create).not.toHaveBeenCalled(); + }); + }); + + describe("provider flow", () => { + beforeEach(() => { + component.providerId = "provider-123"; + }); + + it("should load provider data", async () => { + mockProviderApiService.getProvider.mockResolvedValue({ + id: "provider-123", + name: "Test Provider", + } as any); + + fixture.detectChanges(); + await fixture.whenStable(); + + expect(mockProviderApiService.getProvider).toHaveBeenCalledWith("provider-123"); + expect(component.provider).toBeDefined(); + }); + + it("should default to Teams Annual plan for providers", async () => { + mockProviderApiService.getProvider.mockResolvedValue({} as any); + + fixture.detectChanges(); + await fixture.whenStable(); + + expect(component.plan).toBe(PlanType.TeamsAnnually); + }); + + it("should require clientOwnerEmail for provider flow", async () => { + mockProviderApiService.getProvider.mockResolvedValue({} as any); + + fixture.detectChanges(); + await fixture.whenStable(); + + const clientOwnerEmailControl = component.formGroup.controls.clientOwnerEmail; + clientOwnerEmailControl.setValue(""); + + expect(clientOwnerEmailControl.hasError("required")).toBe(true); + }); + + it("should set businessOwned to true for provider flow", async () => { + mockProviderApiService.getProvider.mockResolvedValue({} as any); + + fixture.detectChanges(); + await fixture.whenStable(); + + expect(component.formGroup.controls.businessOwned.value).toBe(true); + }); + }); + + describe("self-hosted flow", () => { + beforeEach(async () => { + mockPlatformUtilsService.isSelfHost.mockReturnValue(true); + }); + + it("should render organization self-hosted license and not load plans", async () => { + mockPlatformUtilsService.isSelfHost.mockReturnValue(true); + const selfHostedFixture = TestBed.createComponent(OrganizationPlansComponent); + const selfHostedComponent = selfHostedFixture.componentInstance; + + expect(selfHostedComponent.selfHosted).toBe(true); + expect(mockApiService.getPlans).not.toHaveBeenCalled(); + }); + + it("should handle license file upload success", async () => { + const successSpy = jest.spyOn(component.onSuccess, "emit"); + + await component["onLicenseFileUploaded"]("uploaded-org-id"); + + expect(mockToastService.showToast).toHaveBeenCalledWith({ + variant: "success", + title: "organizationCreated", + message: "organizationReadyToGo", + }); + + expect(successSpy).toHaveBeenCalledWith({ + organizationId: "uploaded-org-id", + }); + + expect(mockMessagingService.send).toHaveBeenCalledWith("organizationCreated", { + organizationId: "uploaded-org-id", + }); + }); + + it("should navigate after license upload if not in trial or sponsorship flow", async () => { + component.acceptingSponsorship = false; + component["isInTrialFlow"] = false; + + await component["onLicenseFileUploaded"]("uploaded-org-id"); + + expect(mockRouter.navigate).toHaveBeenCalledWith(["/organizations/uploaded-org-id"]); + }); + + it("should not navigate after license upload if accepting sponsorship", async () => { + component.acceptingSponsorship = true; + + await component["onLicenseFileUploaded"]("uploaded-org-id"); + + expect(mockRouter.navigate).not.toHaveBeenCalled(); + }); + + it("should emit trial success after license upload in trial flow", async () => { + component["isInTrialFlow"] = true; + + fixture.detectChanges(); + await fixture.whenStable(); + + const trialSpy = jest.spyOn(component.onTrialBillingSuccess, "emit"); + + await component["onLicenseFileUploaded"]("uploaded-org-id"); + + expect(trialSpy).toHaveBeenCalled(); + }); + }); + + describe("policy enforcement", () => { + it("should check single org policy", async () => { + mockPolicyService.policyAppliesToUser$.mockReturnValue(of(true)); + + fixture.detectChanges(); + await fixture.whenStable(); + + expect(component.singleOrgPolicyAppliesToActiveUser).toBe(true); + }); + + it("should not block provider flow with single org policy", async () => { + mockPolicyService.policyAppliesToUser$.mockReturnValue(of(true)); + component.providerId = "provider-123"; + mockProviderApiService.getProvider.mockResolvedValue({} as any); + + fixture.detectChanges(); + await fixture.whenStable(); + + expect(component.singleOrgPolicyBlock).toBe(false); + }); + }); + + describe("business ownership change flow", () => { + beforeEach(async () => { + fixture.detectChanges(); + await fixture.whenStable(); + }); + + it("should automatically upgrade to business-compatible plan when marking as business-owned", () => { + // Start with a personal plan + component.formGroup.controls.businessOwned.setValue(false); + component.productTier = ProductTierType.Families; + component.plan = PlanType.FamiliesAnnually; + + // Mark as business-owned + component.formGroup.controls.businessOwned.setValue(true); + component.changedOwnedBusiness(); + + // Should automatically switch to Teams (lowest business plan) + expect(component.formGroup.controls.productTier.value).toBe(ProductTierType.Teams); + expect(component.formGroup.controls.plan.value).toBe(PlanType.TeamsAnnually); + + // Unchecking businessOwned should not force a downgrade + component.formGroup.controls.businessOwned.setValue(false); + component.changedOwnedBusiness(); + + expect(component.formGroup.controls.productTier.value).toBe(ProductTierType.Teams); + }); + }); + + describe("business organization plan selection flow", () => { + beforeEach(async () => { + fixture.detectChanges(); + await fixture.whenStable(); + }); + + it("should restrict available plans based on business ownership and upgrade context", () => { + // Upgrade flow (showFree = false) should exclude Free plan + component.showFree = false; + let products = component.selectableProducts; + expect(products.find((p) => p.type === PlanType.Free)).toBeUndefined(); + + // Create flow (showFree = true) should include Free plan + component.showFree = true; + products = component.selectableProducts; + expect(products.find((p) => p.type === PlanType.Free)).toBeDefined(); + + // Business organizations should only see business-compatible plans + component.formGroup.controls.businessOwned.setValue(true); + products = component.selectableProducts; + const nonFreeBusinessPlans = products.filter((p) => p.type !== PlanType.Free); + nonFreeBusinessPlans.forEach((plan) => { + expect(plan.canBeUsedByBusiness).toBe(true); + }); + }); + }); + + describe("accepting sponsorship flow", () => { + beforeEach(() => { + component.acceptingSponsorship = true; + }); + + it("should configure Families plan with full discount when accepting sponsorship", async () => { + fixture.detectChanges(); + await fixture.whenStable(); + + // Only Families plan should be available + const products = component.selectableProducts; + expect(products.length).toBe(1); + expect(products[0].productTier).toBe(ProductTierType.Families); + + // Full discount should be applied making the base price free + component.productTier = ProductTierType.Families; + component.plan = PlanType.FamiliesAnnually; + + const subtotal = component.passwordManagerSubtotal; + expect(subtotal).toBe(0); // Discount covers the full base price + expect(component.discount).toBe(products[0].PasswordManager.basePrice); + }); + }); + + describe("upgrade flow", () => { + it("should successfully upgrade organization", async () => { + setupMockUpgradeOrganization(mockOrganizationApiService, organizationsSubject, { + maxStorageGb: 2, + }); + + const upgradeFixture = TestBed.createComponent(OrganizationPlansComponent); + const upgradeComponent = upgradeFixture.componentInstance; + upgradeComponent.organizationId = "org-123"; + upgradeComponent.currentPlan = mockPasswordManagerPlans[0]; // Free plan + + upgradeFixture.detectChanges(); + await upgradeFixture.whenStable(); + + upgradeComponent.productTier = ProductTierType.Teams; + upgradeComponent.plan = PlanType.TeamsAnnually; + upgradeComponent.formGroup.controls.additionalSeats.setValue(5); + + mockOrganizationApiService.upgrade.mockResolvedValue(undefined); + + await upgradeComponent.submit(); + + expect(mockOrganizationApiService.upgrade).toHaveBeenCalledWith( + "org-123", + expect.objectContaining({ + planType: PlanType.TeamsAnnually, + additionalSeats: 5, + }), + ); + + expect(mockToastService.showToast).toHaveBeenCalledWith({ + variant: "success", + title: null, + message: "organizationUpgraded", + }); + }); + + it("should handle upgrade requiring payment method", async () => { + setupMockUpgradeOrganization(mockOrganizationApiService, organizationsSubject, { + hasPaymentSource: false, + maxStorageGb: 2, + }); + + const upgradeFixture = TestBed.createComponent(OrganizationPlansComponent); + const upgradeComponent = upgradeFixture.componentInstance; + upgradeComponent.organizationId = "org-123"; + upgradeComponent.showFree = false; // Required for upgradeRequiresPaymentMethod + + upgradeFixture.detectChanges(); + await upgradeFixture.whenStable(); + + expect(upgradeComponent.upgradeRequiresPaymentMethod).toBe(true); + }); + }); + + describe("billing form display flow", () => { + beforeEach(async () => { + fixture.detectChanges(); + await fixture.whenStable(); + }); + + it("should show appropriate billing fields based on plan type", () => { + // Personal plans (Free, Families) should not require tax ID + component.productTier = ProductTierType.Free; + expect(component["showTaxIdField"]).toBe(false); + + component.productTier = ProductTierType.Families; + expect(component["showTaxIdField"]).toBe(false); + + // Business plans (Teams, Enterprise) should show tax ID field + component.productTier = ProductTierType.Teams; + expect(component["showTaxIdField"]).toBe(true); + + component.productTier = ProductTierType.Enterprise; + expect(component["showTaxIdField"]).toBe(true); + }); + }); + + describe("secrets manager handling flow", () => { + beforeEach(async () => { + fixture.detectChanges(); + await fixture.whenStable(); + }); + + it("should prefill SM seats from existing subscription", async () => { + mockOrganization = { + id: "org-123", + name: "Test Org", + productTierType: ProductTierType.Teams, + useSecretsManager: true, + } as Organization; + + organizationsSubject.next([mockOrganization]); + + mockOrganizationApiService.getBilling.mockResolvedValue({ + paymentSource: { type: "card" }, + } as any); + + mockOrganizationApiService.getSubscription.mockResolvedValue({ + planType: PlanType.TeamsAnnually, + smSeats: 5, + smServiceAccounts: 75, + } as any); + + const upgradeFixture = TestBed.createComponent(OrganizationPlansComponent); + const upgradeComponent = upgradeFixture.componentInstance; + upgradeComponent.organizationId = "org-123"; + upgradeComponent.currentPlan = mockPasswordManagerPlans[2]; // Teams plan + + upgradeFixture.detectChanges(); + await upgradeFixture.whenStable(); + + upgradeComponent.changedProduct(); + + expect(upgradeComponent.secretsManagerForm.controls.enabled.value).toBe(true); + expect(upgradeComponent.secretsManagerForm.controls.userSeats.value).toBe(5); + expect(upgradeComponent.secretsManagerForm.controls.additionalServiceAccounts.value).toBe(25); + }); + + it("should enable SM by default when enableSecretsManagerByDefault is true", async () => { + const smFixture = TestBed.createComponent(OrganizationPlansComponent); + const smComponent = smFixture.componentInstance; + smComponent.enableSecretsManagerByDefault = true; + smComponent.productTier = ProductTierType.Teams; + + smFixture.detectChanges(); + await smFixture.whenStable(); + + expect(smComponent.secretsManagerForm.value.enabled).toBe(true); + expect(smComponent.secretsManagerForm.value.userSeats).toBe(1); + expect(smComponent.secretsManagerForm.value.additionalServiceAccounts).toBe(0); + }); + + it("should trigger tax recalculation when SM form changes", fakeAsync(() => { + component.productTier = ProductTierType.Teams; + component.changedProduct(); + component.billingFormGroup.controls.billingAddress.patchValue({ + country: "US", + postalCode: "90210", + }); + + // Clear previous calls + jest.clearAllMocks(); + + // Change SM form + component.secretsManagerForm.patchValue({ + enabled: true, + userSeats: 3, + }); + + tick(1500); // Wait for debounce + + expect( + mockPreviewInvoiceClient.previewTaxForOrganizationSubscriptionPurchase, + ).toHaveBeenCalled(); + })); + }); + + describe("form update helpers flow", () => { + beforeEach(async () => { + fixture.detectChanges(); + await fixture.whenStable(); + }); + + it("should handle premium addon access based on plan features", () => { + // Plan without premium access option should set addon to true (meaning it's included) + component.productTier = ProductTierType.Families; + component.changedProduct(); + + expect(component.formGroup.controls.premiumAccessAddon.value).toBe(true); + + // Plan with premium access option should set addon to false (user can opt-in) + component.productTier = ProductTierType.Teams; + component.changedProduct(); + + expect(component.formGroup.controls.premiumAccessAddon.value).toBe(false); + }); + + it("should handle additional storage for upgrade with existing data", async () => { + mockOrganization = { + id: "org-123", + name: "Test Org", + productTierType: ProductTierType.Free, + maxStorageGb: 5, + } as Organization; + + organizationsSubject.next([mockOrganization]); + + mockOrganizationApiService.getBilling.mockResolvedValue({ + paymentSource: { type: "card" }, + } as any); + + mockOrganizationApiService.getSubscription.mockResolvedValue({ + planType: PlanType.Free, + } as any); + + const upgradeFixture = TestBed.createComponent(OrganizationPlansComponent); + const upgradeComponent = upgradeFixture.componentInstance; + upgradeComponent.organizationId = "org-123"; + upgradeComponent.currentPlan = mockPasswordManagerPlans[0]; // Free plan with 0 GB base + + upgradeFixture.detectChanges(); + await upgradeFixture.whenStable(); + + upgradeComponent.productTier = ProductTierType.Teams; + upgradeComponent.changedProduct(); + + expect(upgradeComponent.formGroup.controls.additionalStorage.value).toBe(5); + }); + + it("should reset additional storage when plan doesn't support it", () => { + component.formGroup.controls.additionalStorage.setValue(10); + component.productTier = ProductTierType.Free; + component.changedProduct(); + + expect(component.formGroup.controls.additionalStorage.value).toBe(0); + }); + + it("should handle additional seats for various scenarios", () => { + // Plan without additional seats option should reset to 0 + component.formGroup.controls.additionalSeats.setValue(10); + component.productTier = ProductTierType.Families; + component.changedProduct(); + + expect(component.formGroup.controls.additionalSeats.value).toBe(0); + + // Default to 1 seat for new org with seats option + component.productTier = ProductTierType.Teams; + component.changedProduct(); + + expect(component.formGroup.controls.additionalSeats.value).toBeGreaterThanOrEqual(1); + }); + + it("should prefill seats from current plan when upgrading from non-seats plan", async () => { + mockOrganization = { + id: "org-123", + name: "Test Org", + productTierType: ProductTierType.Free, + seats: 2, + } as Organization; + + organizationsSubject.next([mockOrganization]); + + mockOrganizationApiService.getBilling.mockResolvedValue({ + paymentSource: { type: "card" }, + } as any); + + mockOrganizationApiService.getSubscription.mockResolvedValue({ + planType: PlanType.Free, + } as any); + + const upgradeFixture = TestBed.createComponent(OrganizationPlansComponent); + const upgradeComponent = upgradeFixture.componentInstance; + upgradeComponent.organizationId = "org-123"; + upgradeComponent.currentPlan = mockPasswordManagerPlans[0]; // Free plan (no additional seats) + + upgradeFixture.detectChanges(); + await upgradeFixture.whenStable(); + + upgradeComponent.productTier = ProductTierType.Teams; + upgradeComponent.changedProduct(); + + // Should use base seats from current plan + expect(upgradeComponent.formGroup.controls.additionalSeats.value).toBe(2); + }); + }); + + describe("provider creation flow", () => { + beforeEach(() => { + component.providerId = "provider-123"; + mockProviderApiService.getProvider.mockResolvedValue({ + id: "provider-123", + name: "Test Provider", + } as any); + }); + + it("should create organization through provider with wrapped key", async () => { + fixture.detectChanges(); + await fixture.whenStable(); + + patchOrganizationForm(component, { + name: "Provider Client Org", + billingEmail: "client@example.com", + productTier: ProductTierType.Teams, + plan: PlanType.TeamsAnnually, + additionalSeats: 5, + }); + component.formGroup.patchValue({ + clientOwnerEmail: "owner@client.com", + }); + + patchBillingAddress(component); + + const mockOrgKey = {} as any; + const mockProviderKey = {} as any; + + mockKeyService.makeOrgKey.mockResolvedValue([ + { encryptedString: "mock-key" }, + mockOrgKey, + ] as any); + + mockEncryptService.encryptString.mockResolvedValue({ + encryptedString: "mock-collection", + } as any); + + mockKeyService.makeKeyPair.mockResolvedValue([ + "public-key", + { encryptedString: "private-key" }, + ] as any); + + mockKeyService.providerKeys$.mockReturnValue(of({ "provider-123": mockProviderKey })); + + mockEncryptService.wrapSymmetricKey.mockResolvedValue({ + encryptedString: "wrapped-key", + } as any); + + mockApiService.postProviderCreateOrganization.mockResolvedValue({ + organizationId: "provider-org-id", + } as any); + + setupMockPaymentMethodComponent(component); + + await component.submit(); + + expect(mockApiService.postProviderCreateOrganization).toHaveBeenCalledWith( + "provider-123", + expect.objectContaining({ + clientOwnerEmail: "owner@client.com", + }), + ); + + expect(mockEncryptService.wrapSymmetricKey).toHaveBeenCalledWith(mockOrgKey, mockProviderKey); + }); + }); + + describe("upgrade with missing keys flow", () => { + beforeEach(async () => { + mockOrganization = { + id: "org-123", + name: "Test Org", + productTierType: ProductTierType.Free, + seats: 5, + hasPublicAndPrivateKeys: false, // Missing keys + } as Organization; + + organizationsSubject.next([mockOrganization]); + + mockOrganizationApiService.getBilling.mockResolvedValue({ + paymentSource: { type: "card" }, + } as any); + + mockOrganizationApiService.getSubscription.mockResolvedValue({ + planType: PlanType.Free, + } as any); + + component.organizationId = "org-123"; + + fixture.detectChanges(); + await fixture.whenStable(); + }); + + it("should backfill organization keys during upgrade", async () => { + component.productTier = ProductTierType.Teams; + component.plan = PlanType.TeamsAnnually; + component.formGroup.controls.additionalSeats.setValue(5); + + const mockOrgShareKey = {} as any; + mockKeyService.orgKeys$.mockReturnValue(of({ "org-123": mockOrgShareKey })); + + mockKeyService.makeKeyPair.mockResolvedValue([ + "public-key", + { encryptedString: "private-key" }, + ] as any); + + mockOrganizationApiService.upgrade.mockResolvedValue(undefined); + + await component.submit(); + + expect(mockOrganizationApiService.upgrade).toHaveBeenCalledWith( + "org-123", + expect.objectContaining({ + keys: expect.any(Object), + }), + ); + }); + }); + + describe("trial flow", () => { + beforeEach(async () => { + fixture.detectChanges(); + await fixture.whenStable(); + }); + + it("should emit onTrialBillingSuccess when in trial flow", async () => { + component["isInTrialFlow"] = true; + const trialSpy = jest.spyOn(component.onTrialBillingSuccess, "emit"); + + component.formGroup.patchValue({ + name: "Trial Org", + billingEmail: "trial@example.com", + productTier: ProductTierType.Enterprise, + plan: PlanType.EnterpriseAnnually, + additionalSeats: 10, + }); + + component.billingFormGroup.controls.billingAddress.patchValue({ + country: "US", + postalCode: "12345", + line1: "123 Street", + city: "City", + state: "CA", + }); + + mockKeyService.makeOrgKey.mockResolvedValue([ + { encryptedString: "mock-key" }, + {} as any, + ] as any); + + mockEncryptService.encryptString.mockResolvedValue({ + encryptedString: "mock-collection", + } as any); + + mockKeyService.makeKeyPair.mockResolvedValue([ + "public-key", + { encryptedString: "private-key" }, + ] as any); + + mockOrganizationApiService.create.mockResolvedValue({ + id: "trial-org-id", + } as any); + + component["enterPaymentMethodComponent"] = { + tokenize: jest.fn().mockResolvedValue({ + token: "mock_token", + type: "card", + }), + } as any; + + await component.submit(); + + expect(trialSpy).toHaveBeenCalledWith({ + orgId: "trial-org-id", + subLabelText: expect.stringContaining("annual"), + }); + }); + + it("should not navigate away when in trial flow", async () => { + component["isInTrialFlow"] = true; + + component.formGroup.patchValue({ + name: "Trial Org", + billingEmail: "trial@example.com", + productTier: ProductTierType.Free, + plan: PlanType.Free, + }); + + mockKeyService.makeOrgKey.mockResolvedValue([ + { encryptedString: "mock-key" }, + {} as any, + ] as any); + + mockEncryptService.encryptString.mockResolvedValue({ + encryptedString: "mock-collection", + } as any); + + mockKeyService.makeKeyPair.mockResolvedValue([ + "public-key", + { encryptedString: "private-key" }, + ] as any); + + mockOrganizationApiService.create.mockResolvedValue({ + id: "trial-org-id", + } as any); + + await component.submit(); + + expect(mockRouter.navigate).not.toHaveBeenCalled(); + }); + }); + + describe("upgrade prefill flow", () => { + it("should prefill Families plan for Free tier upgrade", async () => { + mockOrganization = { + id: "org-123", + name: "Test Org", + productTierType: ProductTierType.Free, + } as Organization; + + organizationsSubject.next([mockOrganization]); + + mockOrganizationApiService.getBilling.mockResolvedValue({ + paymentSource: null, + } as any); + + mockOrganizationApiService.getSubscription.mockResolvedValue({ + planType: PlanType.Free, + } as any); + + const upgradeFixture = TestBed.createComponent(OrganizationPlansComponent); + const upgradeComponent = upgradeFixture.componentInstance; + upgradeComponent.organizationId = "org-123"; + upgradeComponent.currentPlan = mockPasswordManagerPlans[0]; // Free + + upgradeFixture.detectChanges(); + await upgradeFixture.whenStable(); + + expect(upgradeComponent.plan).toBe(PlanType.FamiliesAnnually); + expect(upgradeComponent.productTier).toBe(ProductTierType.Families); + }); + + it("should prefill Teams plan for Families tier upgrade when TeamsStarter unavailable", async () => { + mockOrganization = { + id: "org-123", + name: "Test Org", + productTierType: ProductTierType.Families, + } as Organization; + + organizationsSubject.next([mockOrganization]); + + mockOrganizationApiService.getBilling.mockResolvedValue({ + paymentSource: { type: "card" }, + } as any); + + mockOrganizationApiService.getSubscription.mockResolvedValue({ + planType: PlanType.FamiliesAnnually, + } as any); + + const upgradeFixture = TestBed.createComponent(OrganizationPlansComponent); + const upgradeComponent = upgradeFixture.componentInstance; + upgradeComponent.organizationId = "org-123"; + upgradeComponent.currentPlan = mockPasswordManagerPlans[1]; // Families + + upgradeFixture.detectChanges(); + await upgradeFixture.whenStable(); + + expect(upgradeComponent.plan).toBe(PlanType.TeamsAnnually); + expect(upgradeComponent.productTier).toBe(ProductTierType.Teams); + }); + + it("should use upgradeSortOrder for sequential plan upgrades", async () => { + mockOrganization = { + id: "org-123", + name: "Test Org", + productTierType: ProductTierType.Teams, + } as Organization; + + organizationsSubject.next([mockOrganization]); + + mockOrganizationApiService.getBilling.mockResolvedValue({ + paymentSource: { type: "card" }, + } as any); + + mockOrganizationApiService.getSubscription.mockResolvedValue({ + planType: PlanType.TeamsAnnually, + } as any); + + const upgradeFixture = TestBed.createComponent(OrganizationPlansComponent); + const upgradeComponent = upgradeFixture.componentInstance; + upgradeComponent.organizationId = "org-123"; + upgradeComponent.currentPlan = mockPasswordManagerPlans[2]; // Teams + + upgradeFixture.detectChanges(); + await upgradeFixture.whenStable(); + + expect(upgradeComponent.plan).toBe(PlanType.EnterpriseAnnually); + expect(upgradeComponent.productTier).toBe(ProductTierType.Enterprise); + }); + + it("should not prefill for Enterprise tier (no upgrade available)", async () => { + mockOrganization = { + id: "org-123", + name: "Test Org", + productTierType: ProductTierType.Enterprise, + } as Organization; + + organizationsSubject.next([mockOrganization]); + + mockOrganizationApiService.getBilling.mockResolvedValue({ + paymentSource: { type: "card" }, + } as any); + + mockOrganizationApiService.getSubscription.mockResolvedValue({ + planType: PlanType.EnterpriseAnnually, + } as any); + + const upgradeFixture = TestBed.createComponent(OrganizationPlansComponent); + const upgradeComponent = upgradeFixture.componentInstance; + upgradeComponent.organizationId = "org-123"; + upgradeComponent.currentPlan = mockPasswordManagerPlans[3]; // Enterprise + + upgradeFixture.detectChanges(); + await upgradeFixture.whenStable(); + + // Should not change from default Free + expect(upgradeComponent.productTier).toBe(ProductTierType.Free); + }); + }); + + describe("plan filtering logic", () => { + beforeEach(async () => { + fixture.detectChanges(); + await fixture.whenStable(); + }); + + it("should check if provider is qualified for 2020 plans", () => { + component.providerId = "provider-123"; + component["provider"] = { + id: "provider-123", + creationDate: "2023-01-01", // Before cutoff + } as any; + + const isQualified = component["isProviderQualifiedFor2020Plan"](); + + expect(isQualified).toBe(true); + }); + + it("should not qualify provider created after 2020 plan cutoff", () => { + component.providerId = "provider-123"; + component["provider"] = { + id: "provider-123", + creationDate: "2023-12-01", // After cutoff (2023-11-06) + } as any; + + const isQualified = component["isProviderQualifiedFor2020Plan"](); + + expect(isQualified).toBe(false); + }); + + it("should return false if provider has no creation date", () => { + component.providerId = "provider-123"; + component["provider"] = { + id: "provider-123", + creationDate: null, + } as any; + + const isQualified = component["isProviderQualifiedFor2020Plan"](); + + expect(isQualified).toBe(false); + }); + + it("should exclude upgrade-ineligible plans", async () => { + mockOrganization = { + id: "org-123", + name: "Test Org", + productTierType: ProductTierType.Teams, + } as Organization; + + organizationsSubject.next([mockOrganization]); + + mockOrganizationApiService.getBilling.mockResolvedValue({ + paymentSource: { type: "card" }, + } as any); + + mockOrganizationApiService.getSubscription.mockResolvedValue({ + planType: PlanType.TeamsAnnually, + } as any); + + const upgradeFixture = TestBed.createComponent(OrganizationPlansComponent); + const upgradeComponent = upgradeFixture.componentInstance; + upgradeComponent.organizationId = "org-123"; + upgradeComponent.currentPlan = mockPasswordManagerPlans[2]; // Teams + + upgradeFixture.detectChanges(); + await upgradeFixture.whenStable(); + + const products = upgradeComponent.selectableProducts; + + // Should not include plans with lower or equal upgradeSortOrder + expect(products.find((p) => p.type === PlanType.Free)).toBeUndefined(); + expect(products.find((p) => p.type === PlanType.FamiliesAnnually)).toBeUndefined(); + expect(products.find((p) => p.type === PlanType.TeamsAnnually)).toBeUndefined(); + }); + }); + + describe("helper calculation methods", () => { + beforeEach(async () => { + fixture.detectChanges(); + await fixture.whenStable(); + }); + + it("should calculate monthly seat price correctly", () => { + const annualPlan = mockPasswordManagerPlans[2]; // Teams Annual - 48/year + const monthlyPrice = component.seatPriceMonthly(annualPlan); + + expect(monthlyPrice).toBe(4); // 48 / 12 + }); + + it("should calculate monthly storage price correctly", () => { + const annualPlan = mockPasswordManagerPlans[2]; // 4/GB/year + const monthlyPrice = component.additionalStoragePriceMonthly(annualPlan); + + expect(monthlyPrice).toBeCloseTo(0.333, 2); // 4 / 12 + }); + + it("should generate billing sublabel text for annual plan", () => { + component.productTier = ProductTierType.Teams; + component.plan = PlanType.TeamsAnnually; + + const sublabel = component["billingSubLabelText"](); + + expect(sublabel).toContain("annual"); + expect(sublabel).toContain("$48"); // Seat price + expect(sublabel).toContain("yr"); + }); + + it("should generate billing sublabel text for plan with base price", () => { + component.productTier = ProductTierType.Families; + component.plan = PlanType.FamiliesAnnually; + + const sublabel = component["billingSubLabelText"](); + + expect(sublabel).toContain("annual"); + expect(sublabel).toContain("$40"); // Base price + }); + }); + + describe("template rendering and UI visibility", () => { + beforeEach(async () => { + fixture.detectChanges(); + await fixture.whenStable(); + }); + + it("should control form visibility based on loading state", () => { + // Initially not loading after setup + expect(component.loading).toBe(false); + + // When loading + component.loading = true; + expect(component.loading).toBe(true); + + // When not loading + component.loading = false; + expect(component.loading).toBe(false); + }); + + it("should determine createOrganization based on organizationId", () => { + // Create flow - no organizationId + expect(component.createOrganization).toBe(true); + + // Upgrade flow - has organizationId + component.organizationId = "org-123"; + expect(component.createOrganization).toBe(false); + }); + + it("should calculate passwordManagerSubtotal correctly for paid plans", () => { + component.productTier = ProductTierType.Teams; + component.changedProduct(); + component.formGroup.controls.additionalSeats.setValue(5); + + const subtotal = component.passwordManagerSubtotal; + + expect(typeof subtotal).toBe("number"); + expect(subtotal).toBeGreaterThan(0); + }); + + it("should show payment description based on plan type", () => { + component.productTier = ProductTierType.Teams; + component.changedProduct(); + + const paymentDesc = component.paymentDesc; + + expect(typeof paymentDesc).toBe("string"); + expect(paymentDesc.length).toBeGreaterThan(0); + }); + + it("should display tax ID field for business plans", () => { + component.productTier = ProductTierType.Free; + expect(component["showTaxIdField"]).toBe(false); + + component.productTier = ProductTierType.Families; + expect(component["showTaxIdField"]).toBe(false); + + component.productTier = ProductTierType.Teams; + expect(component["showTaxIdField"]).toBe(true); + + component.productTier = ProductTierType.Enterprise; + expect(component["showTaxIdField"]).toBe(true); + }); + + it("should show single org policy block when applicable", () => { + component.singleOrgPolicyAppliesToActiveUser = false; + expect(component.singleOrgPolicyBlock).toBe(false); + + component.singleOrgPolicyAppliesToActiveUser = true; + expect(component.singleOrgPolicyBlock).toBe(true); + + // But not when has provider + component.providerId = "provider-123"; + expect(component.singleOrgPolicyBlock).toBe(false); + }); + + it("should determine upgrade requires payment method correctly", async () => { + // Create flow - no organization + expect(component.upgradeRequiresPaymentMethod).toBe(false); + + // Create new component with organization setup + const mockOrg = setupMockUpgradeOrganization( + mockOrganizationApiService, + organizationsSubject, + { + productTierType: ProductTierType.Free, + hasPaymentSource: false, + }, + ); + + const upgradeFixture = TestBed.createComponent(OrganizationPlansComponent); + const upgradeComponent = upgradeFixture.componentInstance; + upgradeComponent.organizationId = mockOrg.id; + upgradeComponent.showFree = false; + + upgradeFixture.detectChanges(); + await upgradeFixture.whenStable(); + + expect(upgradeComponent.upgradeRequiresPaymentMethod).toBe(true); + }); + }); + + describe("user interactions and form controls", () => { + beforeEach(async () => { + fixture.detectChanges(); + await fixture.whenStable(); + }); + + it("should update component state when product tier changes", () => { + component.productTier = ProductTierType.Free; + + // Simulate changing product tier + component.productTier = ProductTierType.Teams; + component.formGroup.controls.productTier.setValue(ProductTierType.Teams); + component.changedProduct(); + + expect(component.productTier).toBe(ProductTierType.Teams); + expect(component.formGroup.controls.plan.value).toBe(PlanType.TeamsAnnually); + }); + + it("should update plan when changedOwnedBusiness is called", () => { + component.formGroup.controls.businessOwned.setValue(false); + component.productTier = ProductTierType.Families; + + component.formGroup.controls.businessOwned.setValue(true); + component.changedOwnedBusiness(); + + // Should switch to a business-compatible plan + expect(component.formGroup.controls.productTier.value).toBe(ProductTierType.Teams); + }); + + it("should emit onCanceled when cancel is called", () => { + const cancelSpy = jest.spyOn(component.onCanceled, "emit"); + + component["cancel"](); + + expect(cancelSpy).toHaveBeenCalled(); + }); + + it("should update form value when additional seats changes", () => { + component.productTier = ProductTierType.Teams; + component.changedProduct(); + + component.formGroup.controls.additionalSeats.setValue(10); + + expect(component.formGroup.controls.additionalSeats.value).toBe(10); + }); + + it("should update form value when additional storage changes", () => { + component.productTier = ProductTierType.Teams; + component.changedProduct(); + + component.formGroup.controls.additionalStorage.setValue(5); + + expect(component.formGroup.controls.additionalStorage.value).toBe(5); + }); + + it("should mark form as invalid when required fields are empty", () => { + component.formGroup.controls.name.setValue(""); + component.formGroup.controls.billingEmail.setValue(""); + component.formGroup.markAllAsTouched(); + + expect(component.formGroup.invalid).toBe(true); + }); + + it("should mark form as valid when all required fields are filled correctly", () => { + patchOrganizationForm(component, { + name: "Valid Org", + billingEmail: "valid@example.com", + }); + + expect(component.formGroup.valid).toBe(true); + }); + + it("should calculate subtotals based on form values", () => { + component.productTier = ProductTierType.Teams; + component.changedProduct(); + component.formGroup.controls.additionalSeats.setValue(5); + component.formGroup.controls.additionalStorage.setValue(10); + + const subtotal = component.passwordManagerSubtotal; + + // Should include cost of seats and storage + expect(subtotal).toBeGreaterThan(0); + }); + + it("should enable Secrets Manager form when plan supports it", () => { + // Free plan doesn't offer Secrets Manager + component.productTier = ProductTierType.Free; + component.formGroup.controls.productTier.setValue(ProductTierType.Free); + component.changedProduct(); + expect(component.planOffersSecretsManager).toBe(false); + + // Teams plan offers Secrets Manager + component.productTier = ProductTierType.Teams; + component.formGroup.controls.productTier.setValue(ProductTierType.Teams); + component.changedProduct(); + expect(component.planOffersSecretsManager).toBe(true); + expect(component.secretsManagerForm.disabled).toBe(false); + }); + + it("should update Secrets Manager subtotal when values change", () => { + component.productTier = ProductTierType.Teams; + component.changedProduct(); + + component.secretsManagerForm.patchValue({ + enabled: false, + }); + expect(component.secretsManagerSubtotal).toBe(0); + + component.secretsManagerForm.patchValue({ + enabled: true, + userSeats: 3, + additionalServiceAccounts: 10, + }); + + const smSubtotal = component.secretsManagerSubtotal; + expect(smSubtotal).toBeGreaterThan(0); + }); + }); + + describe("payment method and billing flow", () => { + beforeEach(async () => { + fixture.detectChanges(); + await fixture.whenStable(); + }); + + it("should update payment method during upgrade when required", async () => { + mockOrganization = { + id: "org-123", + name: "Test Org", + productTierType: ProductTierType.Free, + seats: 5, + hasPublicAndPrivateKeys: true, + } as Organization; + + organizationsSubject.next([mockOrganization]); + + mockOrganizationApiService.getBilling.mockResolvedValue({ + paymentSource: null, // No existing payment source + } as any); + + mockOrganizationApiService.getSubscription.mockResolvedValue({ + planType: PlanType.Free, + } as any); + + const upgradeFixture = TestBed.createComponent(OrganizationPlansComponent); + const upgradeComponent = upgradeFixture.componentInstance; + upgradeComponent.organizationId = "org-123"; + upgradeComponent.showFree = false; // Triggers upgradeRequiresPaymentMethod + + upgradeFixture.detectChanges(); + await upgradeFixture.whenStable(); + + upgradeComponent.productTier = ProductTierType.Teams; + upgradeComponent.plan = PlanType.TeamsAnnually; + upgradeComponent.formGroup.controls.additionalSeats.setValue(5); + + upgradeComponent.billingFormGroup.controls.billingAddress.patchValue({ + country: "US", + postalCode: "12345", + line1: "123 Street", + city: "City", + state: "CA", + }); + + upgradeComponent["enterPaymentMethodComponent"] = { + tokenize: jest.fn().mockResolvedValue({ + token: "new_token", + type: "card", + }), + } as any; + + mockOrganizationApiService.upgrade.mockResolvedValue(undefined); + + await upgradeComponent.submit(); + + expect(mockSubscriberBillingClient.updatePaymentMethod).toHaveBeenCalledWith( + { type: "organization", data: mockOrganization }, + { token: "new_token", type: "card" }, + { country: "US", postalCode: "12345" }, + ); + + expect(mockOrganizationApiService.upgrade).toHaveBeenCalled(); + }); + + it("should validate billing form for paid plans during creation", async () => { + component.formGroup.patchValue({ + name: "New Org", + billingEmail: "test@example.com", + productTier: ProductTierType.Teams, + plan: PlanType.TeamsAnnually, + additionalSeats: 5, + }); + + // Invalid billing form - explicitly mark as invalid since we removed validators from mock forms + component.billingFormGroup.controls.billingAddress.patchValue({ + country: "", + postalCode: "", + }); + + await component.submit(); + + expect(mockOrganizationApiService.create).not.toHaveBeenCalled(); + expect(component.billingFormGroup.invalid).toBe(true); + }); + + it("should not require billing validation for Free plan", async () => { + component.formGroup.patchValue({ + name: "Free Org", + billingEmail: "test@example.com", + productTier: ProductTierType.Free, + plan: PlanType.Free, + }); + + // Leave billing form empty + component.billingFormGroup.reset(); + + mockKeyService.makeOrgKey.mockResolvedValue([ + { encryptedString: "mock-key" }, + {} as any, + ] as any); + + mockEncryptService.encryptString.mockResolvedValue({ + encryptedString: "mock-collection", + } as any); + + mockKeyService.makeKeyPair.mockResolvedValue([ + "public-key", + { encryptedString: "private-key" }, + ] as any); + + mockOrganizationApiService.create.mockResolvedValue({ + id: "free-org-id", + } as any); + + await component.submit(); + + expect(mockOrganizationApiService.create).toHaveBeenCalled(); + }); + }); +}); diff --git a/apps/web/src/app/billing/organizations/organization-plans.component.ts b/apps/web/src/app/billing/organizations/organization-plans.component.ts index 3364ce2cbea..73fea30fa83 100644 --- a/apps/web/src/app/billing/organizations/organization-plans.component.ts +++ b/apps/web/src/app/billing/organizations/organization-plans.component.ts @@ -113,8 +113,6 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { // eslint-disable-next-line @angular-eslint/prefer-signals @Input() currentPlan: PlanResponse; - selectedFile: File; - // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals // eslint-disable-next-line @angular-eslint/prefer-signals @Input() @@ -675,9 +673,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { const collectionCt = collection.encryptedString; const orgKeys = await this.keyService.makeKeyPair(orgKey[1]); - orgId = this.selfHosted - ? await this.createSelfHosted(key, collectionCt, orgKeys) - : await this.createCloudHosted(key, collectionCt, orgKeys, orgKey[1], activeUserId); + orgId = await this.createCloudHosted(key, collectionCt, orgKeys, orgKey[1], activeUserId); this.toastService.showToast({ variant: "success", @@ -953,27 +949,6 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { } } - private async createSelfHosted(key: string, collectionCt: string, orgKeys: [string, EncString]) { - if (!this.selectedFile) { - throw new Error(this.i18nService.t("selectFile")); - } - - const fd = new FormData(); - fd.append("license", this.selectedFile); - fd.append("key", key); - fd.append("collectionName", collectionCt); - const response = await this.organizationApiService.createLicense(fd); - const orgId = response.id; - - await this.apiService.refreshIdentityToken(); - - // Org Keys live outside of the OrganizationLicense - add the keys to the org here - const request = new OrganizationKeysRequest(orgKeys[0], orgKeys[1].encryptedString); - await this.organizationApiService.updateKeys(orgId, request); - - return orgId; - } - private billingSubLabelText(): string { const selectedPlan = this.selectedPlan; const price = From 6dea7504a6105d5d34525e4d85eccf375cd7ef3e Mon Sep 17 00:00:00 2001 From: Mike Amirault Date: Wed, 18 Feb 2026 14:49:51 -0500 Subject: [PATCH 18/89] [PM-26732] Remove Chromium ABE importer feature flag (#19039) --- libs/common/src/enums/feature-flag.enum.ts | 2 - .../default-import-metadata.service.ts | 47 +-------- .../services/import-metadata.service.spec.ts | 95 +------------------ 3 files changed, 6 insertions(+), 138 deletions(-) diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index d252f7dcda5..5160e6aa542 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -53,7 +53,6 @@ export enum FeatureFlag { /* Tools */ UseSdkPasswordGenerators = "pm-19976-use-sdk-password-generators", - ChromiumImporterWithABE = "pm-25855-chromium-importer-abe", SendUIRefresh = "pm-28175-send-ui-refresh", SendEmailOTP = "pm-19051-send-email-verification", @@ -120,7 +119,6 @@ export const DefaultFeatureFlagValue = { /* Tools */ [FeatureFlag.UseSdkPasswordGenerators]: FALSE, - [FeatureFlag.ChromiumImporterWithABE]: FALSE, [FeatureFlag.SendUIRefresh]: FALSE, [FeatureFlag.SendEmailOTP]: FALSE, diff --git a/libs/importer/src/services/default-import-metadata.service.ts b/libs/importer/src/services/default-import-metadata.service.ts index 393c498e118..a9e767178aa 100644 --- a/libs/importer/src/services/default-import-metadata.service.ts +++ b/libs/importer/src/services/default-import-metadata.service.ts @@ -1,11 +1,9 @@ -import { combineLatest, map, Observable } from "rxjs"; +import { map, Observable } from "rxjs"; -import { ClientType, DeviceType } from "@bitwarden/common/enums"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { SemanticLogger } from "@bitwarden/common/tools/log"; import { SystemServiceProvider } from "@bitwarden/common/tools/providers"; -import { DataLoader, ImporterMetadata, Importers, ImportersMetadata, Loader } from "../metadata"; +import { ImporterMetadata, Importers, ImportersMetadata } from "../metadata"; import { ImportType } from "../models/import-options"; import { availableLoaders } from "../util"; @@ -15,13 +13,8 @@ export class DefaultImportMetadataService implements ImportMetadataServiceAbstra protected importers: ImportersMetadata = Importers; private logger: SemanticLogger; - private chromiumWithABE$: Observable; - constructor(protected system: SystemServiceProvider) { this.logger = system.log({ type: "ImportMetadataService" }); - this.chromiumWithABE$ = this.system.configService.getFeatureFlag$( - FeatureFlag.ChromiumImporterWithABE, - ); } async init(): Promise { @@ -30,13 +23,13 @@ export class DefaultImportMetadataService implements ImportMetadataServiceAbstra metadata$(type$: Observable): Observable { const client = this.system.environment.getClientType(); - const capabilities$ = combineLatest([type$, this.chromiumWithABE$]).pipe( - map(([type, enabled]) => { + const capabilities$ = type$.pipe( + map((type) => { if (!this.importers) { return { type, loaders: [] }; } - const loaders = this.availableLoaders(this.importers, type, client, enabled); + const loaders = availableLoaders(this.importers, type, client); if (!loaders || loaders.length === 0) { return { type, loaders: [] }; @@ -55,34 +48,4 @@ export class DefaultImportMetadataService implements ImportMetadataServiceAbstra return capabilities$; } - - /** Determine the available loaders for the given import type and client, considering feature flags and environments */ - private availableLoaders( - importers: ImportersMetadata, - type: ImportType, - client: ClientType, - withABESupport: boolean, - ): DataLoader[] | undefined { - let loaders = availableLoaders(importers, type, client); - - if (withABESupport) { - return loaders; - } - - // Special handling for Brave and Chrome CSV imports on Windows Desktop - if (type === "bravecsv" || type === "chromecsv") { - try { - const device = this.system.environment.getDevice(); - const isWindowsDesktop = device === DeviceType.WindowsDesktop; - if (isWindowsDesktop) { - // Exclude the Chromium loader if on Windows Desktop without ABE support - loaders = loaders?.filter((loader) => loader !== Loader.chromium); - } - } catch { - loaders = loaders?.filter((loader) => loader !== Loader.chromium); - } - } - - return loaders; - } } diff --git a/libs/importer/src/services/import-metadata.service.spec.ts b/libs/importer/src/services/import-metadata.service.spec.ts index e16965a69f8..d6c0ff64d87 100644 --- a/libs/importer/src/services/import-metadata.service.spec.ts +++ b/libs/importer/src/services/import-metadata.service.spec.ts @@ -1,9 +1,7 @@ import { mock, MockProxy } from "jest-mock-extended"; -import { BehaviorSubject, Subject, firstValueFrom } from "rxjs"; +import { Subject, firstValueFrom } from "rxjs"; import { ClientType } from "@bitwarden/client-type"; -import { DeviceType } from "@bitwarden/common/enums"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { SystemServiceProvider } from "@bitwarden/common/tools/providers"; @@ -17,13 +15,10 @@ describe("ImportMetadataService", () => { let systemServiceProvider: MockProxy; beforeEach(() => { - const configService = mock(); - const environment = mock(); environment.getClientType.mockReturnValue(ClientType.Desktop); systemServiceProvider = mock({ - configService, environment, log: jest.fn().mockReturnValue({ debug: jest.fn() }), }); @@ -34,7 +29,6 @@ describe("ImportMetadataService", () => { describe("metadata$", () => { let typeSubject: Subject; let mockLogger: { debug: jest.Mock }; - let featureFlagSubject: BehaviorSubject; const environment = mock(); environment.getClientType.mockReturnValue(ClientType.Desktop); @@ -42,13 +36,8 @@ describe("ImportMetadataService", () => { beforeEach(() => { typeSubject = new Subject(); mockLogger = { debug: jest.fn() }; - featureFlagSubject = new BehaviorSubject(false); - - const configService = mock(); - configService.getFeatureFlag$.mockReturnValue(featureFlagSubject); systemServiceProvider = mock({ - configService, environment, log: jest.fn().mockReturnValue(mockLogger), }); @@ -78,7 +67,6 @@ describe("ImportMetadataService", () => { afterEach(() => { typeSubject.complete(); - featureFlagSubject.complete(); }); it("should emit metadata when type$ emits", async () => { @@ -129,86 +117,5 @@ describe("ImportMetadataService", () => { "capabilities updated", ); }); - - it("should update when feature flag changes", async () => { - environment.getDevice.mockReturnValue(DeviceType.WindowsDesktop); - const testType: ImportType = "bravecsv"; // Use bravecsv which supports chromium loader - const emissions: ImporterMetadata[] = []; - - const subscription = sut.metadata$(typeSubject).subscribe((metadata) => { - emissions.push(metadata); - }); - - typeSubject.next(testType); - featureFlagSubject.next(true); - - // Wait for emissions - await new Promise((resolve) => setTimeout(resolve, 0)); - - expect(emissions).toHaveLength(2); - // Disable ABE - chromium loader should be excluded - expect(emissions[0].loaders).not.toContain(Loader.chromium); - // Enabled ABE - chromium loader should be included - expect(emissions[1].loaders).toContain(Loader.chromium); - - subscription.unsubscribe(); - }); - - it("should exclude chromium loader when ABE is disabled and on Windows Desktop", async () => { - environment.getDevice.mockReturnValue(DeviceType.WindowsDesktop); - const testType: ImportType = "bravecsv"; // bravecsv supports both file and chromium loaders - featureFlagSubject.next(false); - - const metadataPromise = firstValueFrom(sut.metadata$(typeSubject)); - typeSubject.next(testType); - - const result = await metadataPromise; - - expect(result.loaders).not.toContain(Loader.chromium); - expect(result.loaders).toContain(Loader.file); - }); - - it("should exclude chromium loader when ABE is disabled and getDevice throws error", async () => { - environment.getDevice.mockImplementation(() => { - throw new Error("Device detection failed"); - }); - const testType: ImportType = "bravecsv"; - featureFlagSubject.next(false); - - const metadataPromise = firstValueFrom(sut.metadata$(typeSubject)); - typeSubject.next(testType); - - const result = await metadataPromise; - - expect(result.loaders).not.toContain(Loader.chromium); - expect(result.loaders).toContain(Loader.file); - }); - - it("should include chromium loader when ABE is disabled and not on Windows Desktop", async () => { - environment.getDevice.mockReturnValue(DeviceType.MacOsDesktop); - const testType: ImportType = "bravecsv"; // bravecsv supports both file and chromium loaders - featureFlagSubject.next(false); - - const metadataPromise = firstValueFrom(sut.metadata$(typeSubject)); - typeSubject.next(testType); - - const result = await metadataPromise; - - expect(result.loaders).toContain(Loader.chromium); - expect(result.loaders).toContain(Loader.file); - }); - - it("should include chromium loader when ABE is enabled regardless of device", async () => { - environment.getDevice.mockReturnValue(DeviceType.MacOsDesktop); - const testType: ImportType = "bravecsv"; // bravecsv supports both file and chromium loaders - featureFlagSubject.next(true); - - const metadataPromise = firstValueFrom(sut.metadata$(typeSubject)); - typeSubject.next(testType); - - const result = await metadataPromise; - - expect(result.loaders).toContain(Loader.chromium); - }); }); }); From bca2ebaca9b53b519e08877e9bf1c25a8d7d3883 Mon Sep 17 00:00:00 2001 From: Jason Ng Date: Wed, 18 Feb 2026 16:22:50 -0500 Subject: [PATCH 19/89] [PM-30122] allow no folders inside browser folder settings (#19041) --- .../src/vault/popup/settings/folders.component.spec.ts | 3 ++- apps/browser/src/vault/popup/settings/folders.component.ts | 7 ------- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/apps/browser/src/vault/popup/settings/folders.component.spec.ts b/apps/browser/src/vault/popup/settings/folders.component.spec.ts index 678e6d3f10e..7e08cc684a1 100644 --- a/apps/browser/src/vault/popup/settings/folders.component.spec.ts +++ b/apps/browser/src/vault/popup/settings/folders.component.spec.ts @@ -94,11 +94,12 @@ describe("FoldersComponent", () => { fixture.detectChanges(); }); - it("removes the last option in the folder array", (done) => { + it("should show all folders", (done) => { component.folders$.subscribe((folders) => { expect(folders).toEqual([ { id: "1", name: "Folder 1" }, { id: "2", name: "Folder 2" }, + { id: "0", name: "No Folder" }, ]); done(); }); diff --git a/apps/browser/src/vault/popup/settings/folders.component.ts b/apps/browser/src/vault/popup/settings/folders.component.ts index b70c17bd6a5..a38f6630949 100644 --- a/apps/browser/src/vault/popup/settings/folders.component.ts +++ b/apps/browser/src/vault/popup/settings/folders.component.ts @@ -53,13 +53,6 @@ export class FoldersComponent { this.folders$ = this.activeUserId$.pipe( filter((userId): userId is UserId => userId !== null), switchMap((userId) => this.folderService.folderViews$(userId)), - map((folders) => { - // Remove the last folder, which is the "no folder" option folder - if (folders.length > 0) { - return folders.slice(0, folders.length - 1); - } - return folders; - }), ); } From 263ec9412433f1c87360b5810184e4e43fd3d5d2 Mon Sep 17 00:00:00 2001 From: John Harrington <84741727+harr1424@users.noreply.github.com> Date: Wed, 18 Feb 2026 14:59:34 -0700 Subject: [PATCH 20/89] [PM-32161] Remove all emails when email list field is cleared and send is saved (#18959) * add new validation criteria to prevent authType.Email with an empty emails field * simplify validation logic --- apps/browser/src/_locales/en/messages.json | 4 +- apps/desktop/src/locales/en/messages.json | 3 ++ apps/web/src/locales/en/messages.json | 3 ++ .../send-details.component.spec.ts | 53 +++++++++++++++++++ .../send-details/send-details.component.ts | 22 +++++++- 5 files changed, 83 insertions(+), 2 deletions(-) diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 5ed97ce0f07..cc99e0abe18 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -6160,10 +6160,12 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, - "downloadBitwardenApps": { "message": "Download Bitwarden apps" }, diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index 85ef3d94001..3f005db0ba8 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -4615,6 +4615,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 4731be36ef5..ba59184a9f9 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -12866,6 +12866,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, diff --git a/libs/tools/send/send-ui/src/send-form/components/send-details/send-details.component.spec.ts b/libs/tools/send/send-ui/src/send-form/components/send-details/send-details.component.spec.ts index f816c9d5ce4..43b2bc7bcd5 100644 --- a/libs/tools/send/send-ui/src/send-form/components/send-details/send-details.component.spec.ts +++ b/libs/tools/send/send-ui/src/send-form/components/send-details/send-details.component.spec.ts @@ -127,4 +127,57 @@ describe("SendDetailsComponent", () => { expect(emailsControl?.validator).toBeNull(); expect(passwordControl?.validator).toBeNull(); }); + + it("should show validation error when emails are cleared while authType is Email", () => { + // Set authType to Email with valid emails + component.sendDetailsForm.patchValue({ + authType: AuthType.Email, + emails: "test@example.com", + }); + expect(component.sendDetailsForm.get("emails")?.valid).toBe(true); + + // Clear emails - should trigger validation error + component.sendDetailsForm.patchValue({ emails: "" }); + expect(component.sendDetailsForm.get("emails")?.valid).toBe(false); + expect(component.sendDetailsForm.get("emails")?.hasError("emailsRequiredForEmailAuth")).toBe( + true, + ); + }); + + it("should clear validation error when authType is changed from Email after clearing emails", () => { + // Set authType to Email and then clear emails + component.sendDetailsForm.patchValue({ + authType: AuthType.Email, + emails: "test@example.com", + }); + component.sendDetailsForm.patchValue({ emails: "" }); + expect(component.sendDetailsForm.get("emails")?.valid).toBe(false); + + // Change authType to None - emails field should become valid (no longer required) + component.sendDetailsForm.patchValue({ authType: AuthType.None }); + expect(component.sendDetailsForm.get("emails")?.valid).toBe(true); + }); + + it("should force user to change authType by blocking form submission when emails are cleared", () => { + // Set up a send with email verification + component.sendDetailsForm.patchValue({ + name: "Test Send", + authType: AuthType.Email, + emails: "user@example.com", + }); + expect(component.sendDetailsForm.valid).toBe(true); + + // User clears emails field + component.sendDetailsForm.patchValue({ emails: "" }); + + // Form should now be invalid, preventing save + expect(component.sendDetailsForm.valid).toBe(false); + expect(component.sendDetailsForm.get("emails")?.hasError("emailsRequiredForEmailAuth")).toBe( + true, + ); + + // User must change authType to continue + component.sendDetailsForm.patchValue({ authType: AuthType.None }); + expect(component.sendDetailsForm.valid).toBe(true); + }); }); diff --git a/libs/tools/send/send-ui/src/send-form/components/send-details/send-details.component.ts b/libs/tools/send/send-ui/src/send-form/components/send-details/send-details.component.ts index ac1453a925c..78681a70a00 100644 --- a/libs/tools/send/send-ui/src/send-form/components/send-details/send-details.component.ts +++ b/libs/tools/send/send-ui/src/send-form/components/send-details/send-details.component.ts @@ -224,7 +224,10 @@ export class SendDetailsComponent implements OnInit { } else if (type === AuthType.Email) { passwordControl.setValue(null); passwordControl.clearValidators(); - emailsControl.setValidators([Validators.required, this.emailListValidator()]); + emailsControl.setValidators([ + this.emailsRequiredForEmailAuthValidator(), + this.emailListValidator(), + ]); } else { emailsControl.setValue(null); emailsControl.clearValidators(); @@ -317,6 +320,23 @@ export class SendDetailsComponent implements OnInit { }; } + emailsRequiredForEmailAuthValidator(): ValidatorFn { + return (control: FormControl): ValidationErrors | null => { + const authType = this.sendDetailsForm?.get("authType")?.value; + const emails = control.value; + + if (authType === AuthType.Email && (!emails || emails.trim() === "")) { + return { + emailsRequiredForEmailAuth: { + message: this.i18nService.t("emailsRequiredChangeAccessType"), + }, + }; + } + + return null; + }; + } + generatePassword = async () => { const on$ = new BehaviorSubject({ source: "send", type: Type.password }); const account$ = this.accountService.activeAccount$.pipe( From f8b5e15a44c1f5770f9057e1d7fd9be7feb8d4fc Mon Sep 17 00:00:00 2001 From: John Harrington <84741727+harr1424@users.noreply.github.com> Date: Wed, 18 Feb 2026 15:08:57 -0700 Subject: [PATCH 21/89] [PM-31731] [Defect] No error is returned when entering an invalid email + an invalid verification code (#18913) * share i18n key for both invalid email and invalid otp submission * claude review --- apps/browser/src/_locales/en/messages.json | 3 +++ apps/cli/src/locales/en/messages.json | 3 +++ apps/desktop/src/locales/en/messages.json | 3 +++ .../app/tools/send/send-access/send-auth.component.ts | 11 ++++++++++- apps/web/src/locales/en/messages.json | 3 +++ 5 files changed, 22 insertions(+), 1 deletion(-) diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index cc99e0abe18..fbfaa17a87d 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ copied", "description": "Value has been copied to the clipboard.", diff --git a/apps/cli/src/locales/en/messages.json b/apps/cli/src/locales/en/messages.json index 18079bd2409..824b03b99cf 100644 --- a/apps/cli/src/locales/en/messages.json +++ b/apps/cli/src/locales/en/messages.json @@ -35,6 +35,9 @@ "invalidVerificationCode": { "message": "Invalid verification code." }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "masterPassRequired": { "message": "Master password is required." }, diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index 3f005db0ba8..97a38235fd7 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Continue" }, diff --git a/apps/web/src/app/tools/send/send-access/send-auth.component.ts b/apps/web/src/app/tools/send/send-access/send-auth.component.ts index 92c3d445333..994bd7f3ee3 100644 --- a/apps/web/src/app/tools/send/send-access/send-auth.component.ts +++ b/apps/web/src/app/tools/send/send-access/send-auth.component.ts @@ -52,6 +52,7 @@ export class SendAuthComponent implements OnInit { authType = AuthType; private expiredAuthAttempts = 0; + private otpSubmitted = false; readonly loading = signal(false); readonly error = signal(false); @@ -184,12 +185,20 @@ export class SendAuthComponent implements OnInit { this.updatePageTitle(); } else if (emailAndOtpRequired(response.error)) { this.enterOtp.set(true); + if (this.otpSubmitted) { + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("invalidEmailOrVerificationCode"), + }); + } + this.otpSubmitted = true; this.updatePageTitle(); } else if (otpInvalid(response.error)) { this.toastService.showToast({ variant: "error", title: this.i18nService.t("errorOccurred"), - message: this.i18nService.t("invalidVerificationCode"), + message: this.i18nService.t("invalidEmailOrVerificationCode"), }); } else if (passwordHashB64Required(response.error)) { this.sendAuthType.set(AuthType.Password); diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index ba59184a9f9..b257a68052d 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -7397,6 +7397,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, From c90b4ded33feb26ea69799ead93989cb989d4a82 Mon Sep 17 00:00:00 2001 From: Meteoni-San <141850520+Meteony@users.noreply.github.com> Date: Wed, 18 Feb 2026 23:22:38 +0100 Subject: [PATCH 22/89] Revert "Inform user if Desktop client already running (#17846)" as per user feedback (#18897) This reverts commit a199744e2456fde1863dba0d89320ac659d04e32. Co-authored-by: neuronull <9162534+neuronull@users.noreply.github.com> --- apps/desktop/src/main/window.main.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/desktop/src/main/window.main.ts b/apps/desktop/src/main/window.main.ts index 2872154aa44..b4ced4471fa 100644 --- a/apps/desktop/src/main/window.main.ts +++ b/apps/desktop/src/main/window.main.ts @@ -4,7 +4,7 @@ import { once } from "node:events"; import * as path from "path"; import * as url from "url"; -import { app, BrowserWindow, dialog, ipcMain, nativeTheme, screen, session } from "electron"; +import { app, BrowserWindow, ipcMain, nativeTheme, screen, session } from "electron"; import { concatMap, firstValueFrom, pairwise } from "rxjs"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -127,7 +127,6 @@ export class WindowMain { if (!isMacAppStore()) { const gotTheLock = app.requestSingleInstanceLock(); if (!gotTheLock) { - dialog.showErrorBox("Error", "An instance of Bitwarden Desktop is already running."); app.quit(); return; } else { From d1250cf5a4449501bd1c3d0c7b0c8cab5f16d129 Mon Sep 17 00:00:00 2001 From: Jackson Engstrom Date: Wed, 18 Feb 2026 14:34:17 -0800 Subject: [PATCH 23/89] [PM-26704] Vault List Item Ordering for Extension (#18853) * shows all/filtered ciphers in allItems instead of the ones that haven't been bubbled up into autofill or favorites * removes remainingCiphers$ remnants * updates loading$ observable logic * updates loading$ test --- .../components/vault/vault.component.html | 2 +- .../popup/components/vault/vault.component.ts | 1 - .../vault-popup-items.service.spec.ts | 35 ++----------------- .../services/vault-popup-items.service.ts | 22 +----------- 4 files changed, 4 insertions(+), 56 deletions(-) diff --git a/apps/browser/src/vault/popup/components/vault/vault.component.html b/apps/browser/src/vault/popup/components/vault/vault.component.html index 28abb92b8a9..2f43d29d776 100644 --- a/apps/browser/src/vault/popup/components/vault/vault.component.html +++ b/apps/browser/src/vault/popup/components/vault/vault.component.html @@ -127,7 +127,7 @@ { }); }); - describe("remainingCiphers$", () => { - beforeEach(() => { - searchService.isSearchable.mockImplementation(async (text) => text.length > 2); - }); - - it("should exclude autofill and favorite ciphers", (done) => { - service.remainingCiphers$.subscribe((ciphers) => { - // 2 autofill ciphers, 2 favorite ciphers = 6 remaining ciphers to show - expect(ciphers.length).toBe(6); - done(); - }); - }); - - it("should filter remainingCiphers$ down to search term", (done) => { - const cipherList = Object.values(allCiphers); - const searchText = "Login"; - - searchService.searchCiphers.mockImplementation(async () => { - return cipherList.filter((cipher) => { - return cipher.name.includes(searchText); - }); - }); - - service.remainingCiphers$.subscribe((ciphers) => { - // There are 6 remaining ciphers but only 2 with "Login" in the name - expect(ciphers.length).toBe(2); - done(); - }); - }); - }); - describe("emptyVault$", () => { it("should return true if there are no ciphers", (done) => { cipherServiceMock.cipherListViews$.mockReturnValue(of([])); @@ -493,8 +462,8 @@ describe("VaultPopupItemsService", () => { // Start tracking loading$ emissions tracked = new ObservableTracker(service.loading$); - // Track remainingCiphers$ to make cipher observables active - trackedCiphers = new ObservableTracker(service.remainingCiphers$); + // Track favoriteCiphers$ to make cipher observables active + trackedCiphers = new ObservableTracker(service.favoriteCiphers$); }); it("should initialize with true first", async () => { diff --git a/apps/browser/src/vault/popup/services/vault-popup-items.service.ts b/apps/browser/src/vault/popup/services/vault-popup-items.service.ts index 016fa330a38..0055d683f22 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-items.service.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-items.service.ts @@ -2,7 +2,6 @@ import { inject, Injectable, NgZone } from "@angular/core"; import { toObservable } from "@angular/core/rxjs-interop"; import { combineLatest, - concatMap, distinctUntilChanged, distinctUntilKeyChanged, filter, @@ -242,31 +241,12 @@ export class VaultPopupItemsService { shareReplay({ refCount: false, bufferSize: 1 }), ); - /** - * List of all remaining ciphers that are not currently suggested for autofill or marked as favorite. - * Ciphers are sorted by name. - */ - remainingCiphers$: Observable = this.favoriteCiphers$.pipe( - concatMap( - ( - favoriteCiphers, // concatMap->of is used to make withLatestFrom lazy to avoid race conditions with autoFillCiphers$ - ) => - of(favoriteCiphers).pipe(withLatestFrom(this._filteredCipherList$, this.autoFillCiphers$)), - ), - map(([favoriteCiphers, ciphers, autoFillCiphers]) => - ciphers.filter( - (cipher) => !autoFillCiphers.includes(cipher) && !favoriteCiphers.includes(cipher), - ), - ), - shareReplay({ refCount: false, bufferSize: 1 }), - ); - /** * Observable that indicates whether the service is currently loading ciphers. */ loading$: Observable = merge( this._ciphersLoading$.pipe(map(() => true)), - this.remainingCiphers$.pipe(map(() => false)), + this.favoriteCiphers$.pipe(map(() => false)), ).pipe(startWith(true), distinctUntilChanged(), shareReplay({ refCount: false, bufferSize: 1 })); /** Observable that indicates whether there is search text present. From 1efd74daafd8d4488ef3fb3cb1f512bd5a04b85c Mon Sep 17 00:00:00 2001 From: Leslie Xiong Date: Wed, 18 Feb 2026 17:59:18 -0500 Subject: [PATCH 24/89] fixed berry styles for dark mode (#19068) --- libs/components/src/berry/berry.component.ts | 4 ++-- libs/components/src/berry/berry.stories.ts | 8 +++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/libs/components/src/berry/berry.component.ts b/libs/components/src/berry/berry.component.ts index 8e58b888f39..a6544b75f6e 100644 --- a/libs/components/src/berry/berry.component.ts +++ b/libs/components/src/berry/berry.component.ts @@ -38,7 +38,7 @@ export class BerryComponent { }); protected readonly textColor = computed(() => { - return this.variant() === "contrast" ? "tw-text-fg-dark" : "tw-text-fg-white"; + return this.variant() === "contrast" ? "tw-text-fg-heading" : "tw-text-fg-contrast"; }); protected readonly padding = computed(() => { @@ -67,7 +67,7 @@ export class BerryComponent { warning: "tw-bg-bg-warning", danger: "tw-bg-bg-danger", accentPrimary: "tw-bg-fg-accent-primary-strong", - contrast: "tw-bg-bg-white", + contrast: "tw-bg-bg-primary", }; return [ diff --git a/libs/components/src/berry/berry.stories.ts b/libs/components/src/berry/berry.stories.ts index 0b71e7259d8..56ee87d9ce3 100644 --- a/libs/components/src/berry/berry.stories.ts +++ b/libs/components/src/berry/berry.stories.ts @@ -75,7 +75,9 @@ export const statusType: Story = { - +
+ +
`, }), @@ -153,8 +155,8 @@ export const AllVariants: Story = { -
- Contrast: +
+ Contrast: From c9b821262c5f1589571645e44dddd02ec8bb51b1 Mon Sep 17 00:00:00 2001 From: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> Date: Wed, 18 Feb 2026 17:08:33 -0600 Subject: [PATCH 25/89] [PM-30927] Fix lock component initialization bug (#18822) --- .../lock/components/lock.component.spec.ts | 148 +++++++++++++++++- .../src/lock/components/lock.component.ts | 29 ++-- 2 files changed, 161 insertions(+), 16 deletions(-) diff --git a/libs/key-management-ui/src/lock/components/lock.component.spec.ts b/libs/key-management-ui/src/lock/components/lock.component.spec.ts index 47c4d14fc98..915f8a2d30e 100644 --- a/libs/key-management-ui/src/lock/components/lock.component.spec.ts +++ b/libs/key-management-ui/src/lock/components/lock.component.spec.ts @@ -1,4 +1,4 @@ -import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { ComponentFixture, TestBed, fakeAsync, tick } from "@angular/core/testing"; import { FormBuilder, ReactiveFormsModule } from "@angular/forms"; import { ActivatedRoute, Router } from "@angular/router"; import { mock } from "jest-mock-extended"; @@ -605,4 +605,150 @@ describe("LockComponent", () => { expect(component.activeUnlockOption).toBe(UnlockOption.Biometrics); }); }); + + describe("listenForUnlockOptionsChanges", () => { + const mockActiveAccount: Account = { + id: userId, + email: "test@example.com", + name: "Test User", + } as Account; + + const mockUnlockOptions: UnlockOptions = { + masterPassword: { enabled: true }, + pin: { enabled: false }, + biometrics: { enabled: false, biometricsStatus: BiometricsStatus.Available }, + prf: { enabled: false }, + }; + + beforeEach(() => { + (component as any).loading = false; + component.activeAccount = mockActiveAccount; + component.activeUnlockOption = null; + component.unlockOptions = null; + mockLockComponentService.getAvailableUnlockOptions$.mockReturnValue(of(mockUnlockOptions)); + }); + + it("skips polling when loading is true", fakeAsync(() => { + (component as any).loading = true; + + component["listenForUnlockOptionsChanges"](); + tick(0); + + expect(mockLockComponentService.getAvailableUnlockOptions$).not.toHaveBeenCalled(); + })); + + it("skips polling when activeAccount is null", fakeAsync(() => { + component.activeAccount = null; + + component["listenForUnlockOptionsChanges"](); + tick(0); + + expect(mockLockComponentService.getAvailableUnlockOptions$).not.toHaveBeenCalled(); + })); + + it("fetches unlock options when loading is false and activeAccount exists", fakeAsync(() => { + component["listenForUnlockOptionsChanges"](); + tick(0); + + expect(mockLockComponentService.getAvailableUnlockOptions$).toHaveBeenCalledWith(userId); + expect(component.unlockOptions).toEqual(mockUnlockOptions); + })); + + it("calls getAvailableUnlockOptions$ at 1000ms intervals", fakeAsync(() => { + component["listenForUnlockOptionsChanges"](); + + // Initial timer fire at 0ms + tick(0); + expect(mockLockComponentService.getAvailableUnlockOptions$).toHaveBeenCalledTimes(1); + + // First poll at 1000ms + tick(1000); + expect(mockLockComponentService.getAvailableUnlockOptions$).toHaveBeenCalledTimes(2); + + // Second poll at 2000ms + tick(1000); + expect(mockLockComponentService.getAvailableUnlockOptions$).toHaveBeenCalledTimes(3); + })); + + it("calls setDefaultActiveUnlockOption when activeUnlockOption is null", fakeAsync(() => { + component.activeUnlockOption = null; + const setDefaultSpy = jest.spyOn(component as any, "setDefaultActiveUnlockOption"); + + component["listenForUnlockOptionsChanges"](); + tick(0); + + expect(setDefaultSpy).toHaveBeenCalledWith(mockUnlockOptions); + })); + + it("does NOT call setDefaultActiveUnlockOption when activeUnlockOption is already set", fakeAsync(() => { + component.activeUnlockOption = UnlockOption.MasterPassword; + component.unlockOptions = mockUnlockOptions; + + const setDefaultSpy = jest.spyOn(component as any, "setDefaultActiveUnlockOption"); + + component["listenForUnlockOptionsChanges"](); + tick(0); + + expect(setDefaultSpy).not.toHaveBeenCalled(); + })); + + it("calls setDefaultActiveUnlockOption when biometrics becomes enabled", fakeAsync(() => { + component.activeUnlockOption = UnlockOption.MasterPassword; + + // Start with biometrics disabled + component.unlockOptions = { + masterPassword: { enabled: true }, + pin: { enabled: false }, + biometrics: { enabled: false, biometricsStatus: BiometricsStatus.Available }, + prf: { enabled: false }, + }; + + // Mock response with biometrics enabled + const newUnlockOptions: UnlockOptions = { + masterPassword: { enabled: true }, + pin: { enabled: false }, + biometrics: { enabled: true, biometricsStatus: BiometricsStatus.Available }, + prf: { enabled: false }, + }; + + mockLockComponentService.getAvailableUnlockOptions$.mockReturnValue(of(newUnlockOptions)); + + const setDefaultSpy = jest.spyOn(component as any, "setDefaultActiveUnlockOption"); + const handleBioSpy = jest.spyOn(component as any, "handleBiometricsUnlockEnabled"); + + component["listenForUnlockOptionsChanges"](); + tick(0); + + expect(setDefaultSpy).toHaveBeenCalledWith(newUnlockOptions); + expect(handleBioSpy).toHaveBeenCalled(); + })); + + it("does NOT call setDefaultActiveUnlockOption when biometrics was already enabled", fakeAsync(() => { + component.activeUnlockOption = UnlockOption.MasterPassword; + + // Start with biometrics already enabled + component.unlockOptions = { + masterPassword: { enabled: true }, + pin: { enabled: false }, + biometrics: { enabled: true, biometricsStatus: BiometricsStatus.Available }, + prf: { enabled: false }, + }; + + // Mock response with biometrics still enabled + const newUnlockOptions: UnlockOptions = { + masterPassword: { enabled: true }, + pin: { enabled: false }, + biometrics: { enabled: true, biometricsStatus: BiometricsStatus.Available }, + prf: { enabled: false }, + }; + mockLockComponentService.getAvailableUnlockOptions$.mockReturnValue(of(newUnlockOptions)); + + const setDefaultSpy = jest.spyOn(component as any, "setDefaultActiveUnlockOption"); + + component["listenForUnlockOptionsChanges"](); + tick(0); + + expect(setDefaultSpy).not.toHaveBeenCalled(); + })); + }); }); diff --git a/libs/key-management-ui/src/lock/components/lock.component.ts b/libs/key-management-ui/src/lock/components/lock.component.ts index 9900aa6e827..5686e4b334a 100644 --- a/libs/key-management-ui/src/lock/components/lock.component.ts +++ b/libs/key-management-ui/src/lock/components/lock.component.ts @@ -202,7 +202,8 @@ export class LockComponent implements OnInit, OnDestroy { timer(0, 1000) .pipe( mergeMap(async () => { - if (this.activeAccount?.id != null) { + // Only perform polling after the component has loaded. This prevents multiple sources setting the default active unlock option on initialization. + if (this.loading === false && this.activeAccount?.id != null) { const prevBiometricsEnabled = this.unlockOptions?.biometrics.enabled; this.unlockOptions = await firstValueFrom( @@ -210,7 +211,6 @@ export class LockComponent implements OnInit, OnDestroy { ); if (this.activeUnlockOption == null) { - this.loading = false; await this.setDefaultActiveUnlockOption(this.unlockOptions); } else if (!prevBiometricsEnabled && this.unlockOptions?.biometrics.enabled) { await this.setDefaultActiveUnlockOption(this.unlockOptions); @@ -275,19 +275,18 @@ export class LockComponent implements OnInit, OnDestroy { this.lockComponentService.getAvailableUnlockOptions$(activeAccount.id), ); - const canUseBiometrics = [ - BiometricsStatus.Available, - ...BIOMETRIC_UNLOCK_TEMPORARY_UNAVAILABLE_STATUSES, - ].includes(await this.biometricService.getBiometricsStatusForUser(activeAccount.id)); - if ( - !this.unlockOptions?.masterPassword.enabled && - !this.unlockOptions?.pin.enabled && - !canUseBiometrics - ) { - // User has no available unlock options, force logout. This happens for TDE users without a masterpassword, that don't have a persistent unlock method set. - this.logService.warning("[LockComponent] User cannot unlock again. Logging out!"); - await this.logoutService.logout(activeAccount.id); - return; + // The canUseBiometrics query is an expensive operation. Only call if both PIN and master password unlock are unavailable. + if (!this.unlockOptions?.masterPassword.enabled && !this.unlockOptions?.pin.enabled) { + const canUseBiometrics = [ + BiometricsStatus.Available, + ...BIOMETRIC_UNLOCK_TEMPORARY_UNAVAILABLE_STATUSES, + ].includes(await this.biometricService.getBiometricsStatusForUser(activeAccount.id)); + if (!canUseBiometrics) { + // User has no available unlock options, force logout. This happens for TDE users without a masterpassword, that don't have a persistent unlock method set. + this.logService.warning("[LockComponent] User cannot unlock again. Logging out!"); + await this.logoutService.logout(activeAccount.id); + return; + } } await this.setDefaultActiveUnlockOption(this.unlockOptions); From 6498ec42f8e2433369e6686c5b658a4d8d8aa835 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Thu, 19 Feb 2026 14:04:43 +0100 Subject: [PATCH 26/89] [BEEEP] Add util functions for uint8 array conversion (#18451) * Add util functions for uint8 array conversion * Use polyfill instead of old functionality * Replace last usage of old functions --- libs/common/src/platform/misc/utils.spec.ts | 140 +++++++++++++++++++- libs/common/src/platform/misc/utils.ts | 76 ++++++++++- 2 files changed, 213 insertions(+), 3 deletions(-) diff --git a/libs/common/src/platform/misc/utils.spec.ts b/libs/common/src/platform/misc/utils.spec.ts index 664c6e22b3a..032b03fc3e2 100644 --- a/libs/common/src/platform/misc/utils.spec.ts +++ b/libs/common/src/platform/misc/utils.spec.ts @@ -417,6 +417,142 @@ describe("Utils Service", () => { // }); }); + describe("fromArrayToHex(...)", () => { + const originalIsNode = Utils.isNode; + + afterEach(() => { + Utils.isNode = originalIsNode; + }); + + runInBothEnvironments("should convert a Uint8Array to a hex string", () => { + const arr = new Uint8Array([0x00, 0x01, 0x02, 0x0a, 0xff]); + const hexString = Utils.fromArrayToHex(arr); + expect(hexString).toBe("0001020aff"); + }); + + runInBothEnvironments("should return null for null input", () => { + const hexString = Utils.fromArrayToHex(null); + expect(hexString).toBeNull(); + }); + + runInBothEnvironments("should return empty string for an empty Uint8Array", () => { + const arr = new Uint8Array([]); + const hexString = Utils.fromArrayToHex(arr); + expect(hexString).toBe(""); + }); + }); + + describe("fromArrayToB64(...)", () => { + const originalIsNode = Utils.isNode; + + afterEach(() => { + Utils.isNode = originalIsNode; + }); + + runInBothEnvironments("should convert a Uint8Array to a b64 string", () => { + const arr = new Uint8Array(asciiHelloWorldArray); + const b64String = Utils.fromArrayToB64(arr); + expect(b64String).toBe(b64HelloWorldString); + }); + + runInBothEnvironments("should return null for null input", () => { + const b64String = Utils.fromArrayToB64(null); + expect(b64String).toBeNull(); + }); + + runInBothEnvironments("should return empty string for an empty Uint8Array", () => { + const arr = new Uint8Array([]); + const b64String = Utils.fromArrayToB64(arr); + expect(b64String).toBe(""); + }); + }); + + describe("fromArrayToUrlB64(...)", () => { + const originalIsNode = Utils.isNode; + + afterEach(() => { + Utils.isNode = originalIsNode; + }); + + runInBothEnvironments("should convert a Uint8Array to a URL-safe b64 string", () => { + // Input that produces +, /, and = in standard base64 + const arr = new Uint8Array([251, 255, 254]); + const urlB64String = Utils.fromArrayToUrlB64(arr); + // Standard b64 would be "+//+" with padding, URL-safe removes padding and replaces chars + expect(urlB64String).not.toContain("+"); + expect(urlB64String).not.toContain("/"); + expect(urlB64String).not.toContain("="); + }); + + runInBothEnvironments("should return null for null input", () => { + const urlB64String = Utils.fromArrayToUrlB64(null); + expect(urlB64String).toBeNull(); + }); + + runInBothEnvironments("should return empty string for an empty Uint8Array", () => { + const arr = new Uint8Array([]); + const urlB64String = Utils.fromArrayToUrlB64(arr); + expect(urlB64String).toBe(""); + }); + }); + + describe("fromArrayToByteString(...)", () => { + const originalIsNode = Utils.isNode; + + afterEach(() => { + Utils.isNode = originalIsNode; + }); + + runInBothEnvironments("should convert a Uint8Array to a byte string", () => { + const arr = new Uint8Array(asciiHelloWorldArray); + const byteString = Utils.fromArrayToByteString(arr); + expect(byteString).toBe(asciiHelloWorld); + }); + + runInBothEnvironments("should return null for null input", () => { + const byteString = Utils.fromArrayToByteString(null); + expect(byteString).toBeNull(); + }); + + runInBothEnvironments("should return empty string for an empty Uint8Array", () => { + const arr = new Uint8Array([]); + const byteString = Utils.fromArrayToByteString(arr); + expect(byteString).toBe(""); + }); + }); + + describe("fromArrayToUtf8(...)", () => { + const originalIsNode = Utils.isNode; + + afterEach(() => { + Utils.isNode = originalIsNode; + }); + + runInBothEnvironments("should convert a Uint8Array to a UTF-8 string", () => { + const arr = new Uint8Array(asciiHelloWorldArray); + const utf8String = Utils.fromArrayToUtf8(arr); + expect(utf8String).toBe(asciiHelloWorld); + }); + + runInBothEnvironments("should return null for null input", () => { + const utf8String = Utils.fromArrayToUtf8(null); + expect(utf8String).toBeNull(); + }); + + runInBothEnvironments("should return empty string for an empty Uint8Array", () => { + const arr = new Uint8Array([]); + const utf8String = Utils.fromArrayToUtf8(arr); + expect(utf8String).toBe(""); + }); + + runInBothEnvironments("should handle multi-byte UTF-8 characters", () => { + // "日本" in UTF-8 bytes + const arr = new Uint8Array([0xe6, 0x97, 0xa5, 0xe6, 0x9c, 0xac]); + const utf8String = Utils.fromArrayToUtf8(arr); + expect(utf8String).toBe("日本"); + }); + }); + describe("Base64 and ArrayBuffer round trip conversions", () => { const originalIsNode = Utils.isNode; @@ -447,10 +583,10 @@ describe("Utils Service", () => { "should correctly round trip convert from base64 to ArrayBuffer and back", () => { // Convert known base64 string to ArrayBuffer - const bufferFromB64 = Utils.fromB64ToArray(b64HelloWorldString).buffer; + const bufferFromB64 = Utils.fromB64ToArray(b64HelloWorldString); // Convert the ArrayBuffer back to a base64 string - const roundTrippedB64String = Utils.fromBufferToB64(bufferFromB64); + const roundTrippedB64String = Utils.fromArrayToB64(bufferFromB64); // Compare the original base64 string with the round-tripped base64 string expect(roundTrippedB64String).toBe(b64HelloWorldString); diff --git a/libs/common/src/platform/misc/utils.ts b/libs/common/src/platform/misc/utils.ts index bdbfc4ea17b..c2d8871c2c9 100644 --- a/libs/common/src/platform/misc/utils.ts +++ b/libs/common/src/platform/misc/utils.ts @@ -8,6 +8,8 @@ import { Observable, of, switchMap } from "rxjs"; import { getHostname, parse } from "tldts"; import { Merge } from "type-fest"; +import "core-js/proposals/array-buffer-base64"; + // This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. // eslint-disable-next-line no-restricted-imports import { KeyService } from "@bitwarden/key-management"; @@ -129,6 +131,78 @@ export class Utils { return arr; } + /** + * Converts a Uint8Array to a hexadecimal string. + * @param arr - The Uint8Array to convert. + * @returns The hexadecimal string representation, or null if the input is null. + */ + static fromArrayToHex(arr: Uint8Array | null): string | null { + if (arr == null) { + return null; + } + + // @ts-expect-error - polyfilled by core-js + return arr.toHex(); + } + + /** + * Converts a Uint8Array to a Base64 encoded string. + * @param arr - The Uint8Array to convert. + * @returns The Base64 encoded string, or null if the input is null. + */ + static fromArrayToB64(arr: Uint8Array | null): string | null { + if (arr == null) { + return null; + } + + // @ts-expect-error - polyfilled by core-js + return arr.toBase64({ alphabet: "base64" }); + } + + /** + * Converts a Uint8Array to a URL-safe Base64 encoded string. + * @param arr - The Uint8Array to convert. + * @returns The URL-safe Base64 encoded string, or null if the input is null. + */ + static fromArrayToUrlB64(arr: Uint8Array | null): string | null { + if (arr == null) { + return null; + } + + // @ts-expect-error - polyfilled by core-js + return arr.toBase64({ alphabet: "base64url" }); + } + + /** + * Converts a Uint8Array to a byte string (each byte as a character). + * @param arr - The Uint8Array to convert. + * @returns The byte string representation, or null if the input is null. + */ + static fromArrayToByteString(arr: Uint8Array | null): string | null { + if (arr == null) { + return null; + } + + let byteString = ""; + for (let i = 0; i < arr.length; i++) { + byteString += String.fromCharCode(arr[i]); + } + return byteString; + } + + /** + * Converts a Uint8Array to a UTF-8 decoded string. + * @param arr - The Uint8Array containing UTF-8 encoded bytes. + * @returns The decoded UTF-8 string, or null if the input is null. + */ + static fromArrayToUtf8(arr: Uint8Array | null): string | null { + if (arr == null) { + return null; + } + + return BufferLib.from(arr).toString("utf8"); + } + /** * Convert binary data into a Base64 string. * @@ -302,7 +376,7 @@ export class Utils { } static fromUtf8ToUrlB64(utfStr: string): string { - return Utils.fromBufferToUrlB64(Utils.fromUtf8ToArray(utfStr)); + return Utils.fromArrayToUrlB64(Utils.fromUtf8ToArray(utfStr)); } static fromB64ToUtf8(b64Str: string): string { From e66a1f37b5a31513bef5d26b7c145b01eaa40a07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9C=A8=20Audrey=20=E2=9C=A8?= Date: Thu, 19 Feb 2026 08:45:24 -0500 Subject: [PATCH 27/89] Extract urlOriginsMatch utility and refactor senderIsInternal (#19076) Adds urlOriginsMatch to @bitwarden/platform, which compares two URLs by scheme, host, and port. Uses `protocol + "//" + host` rather than `URL.origin` because non-special schemes (e.g. chrome-extension://) return the opaque string "null" from .origin, making equality comparison unreliable. URLs without a host (file:, data:) are explicitly rejected to prevent hostless schemes from comparing equal. Refactors senderIsInternal to delegate to urlOriginsMatch and to derive the extension URL via BrowserApi.getRuntimeURL("") rather than inline chrome/browser API detection. Adds full test coverage for senderIsInternal. The previous string-based comparison used startsWith after stripping trailing slashes, which was safe in senderIsInternal where inputs are tightly constrained. As a general utility accepting arbitrary URLs, startsWith can produce false positives (e.g. "https://example.com" matching "https://example.com.evil.com"). Structural host comparison is the correct contract for unrestricted input. --- .../src/platform/browser/browser-api.spec.ts | 100 ++++++++++++++++++ .../src/platform/browser/browser-api.ts | 36 ++++--- libs/platform/src/index.ts | 1 + libs/platform/src/util.spec.ts | 54 ++++++++++ libs/platform/src/util.ts | 53 ++++++++++ 5 files changed, 227 insertions(+), 17 deletions(-) create mode 100644 libs/platform/src/util.spec.ts create mode 100644 libs/platform/src/util.ts diff --git a/apps/browser/src/platform/browser/browser-api.spec.ts b/apps/browser/src/platform/browser/browser-api.spec.ts index f7561b2b50b..d8a8fe52570 100644 --- a/apps/browser/src/platform/browser/browser-api.spec.ts +++ b/apps/browser/src/platform/browser/browser-api.spec.ts @@ -1,5 +1,7 @@ import { mock } from "jest-mock-extended"; +import { LogService } from "@bitwarden/logging"; + import { BrowserApi } from "./browser-api"; type ChromeSettingsGet = chrome.types.ChromeSetting["get"]; @@ -29,6 +31,104 @@ describe("BrowserApi", () => { }); }); + describe("senderIsInternal", () => { + const EXTENSION_ORIGIN = "chrome-extension://id"; + + beforeEach(() => { + jest.spyOn(BrowserApi, "getRuntimeURL").mockReturnValue(`${EXTENSION_ORIGIN}/`); + }); + + it("returns false when sender is undefined", () => { + const result = BrowserApi.senderIsInternal(undefined); + + expect(result).toBe(false); + }); + + it("returns false when sender has no origin", () => { + const result = BrowserApi.senderIsInternal({ id: "abc" } as any); + + expect(result).toBe(false); + }); + + it("returns false when the extension URL cannot be determined", () => { + jest.spyOn(BrowserApi, "getRuntimeURL").mockReturnValue(""); + + const result = BrowserApi.senderIsInternal({ origin: EXTENSION_ORIGIN }); + + expect(result).toBe(false); + }); + + it.each([ + ["an external origin", "https://evil.com"], + ["a subdomain of the extension origin", "chrome-extension://id.evil.com"], + ["a file: URL (opaque origin)", "file:///home/user/page.html"], + ["a data: URL (opaque origin)", "data:text/html,

hi

"], + ])("returns false when sender origin is %s", (_, senderOrigin) => { + const result = BrowserApi.senderIsInternal({ origin: senderOrigin }); + + expect(result).toBe(false); + }); + + it("returns false when sender is from a non-top-level frame", () => { + const result = BrowserApi.senderIsInternal({ origin: EXTENSION_ORIGIN, frameId: 5 }); + + expect(result).toBe(false); + }); + + it("returns true when sender origin matches and no frameId is present (popup)", () => { + const result = BrowserApi.senderIsInternal({ origin: EXTENSION_ORIGIN }); + + expect(result).toBe(true); + }); + + it("returns true when sender origin matches and frameId is 0 (top-level frame)", () => { + const result = BrowserApi.senderIsInternal({ origin: EXTENSION_ORIGIN, frameId: 0 }); + + expect(result).toBe(true); + }); + + it("calls logger.warning when sender has no origin", () => { + const logger = mock(); + + BrowserApi.senderIsInternal({} as any, logger); + + expect(logger.warning).toHaveBeenCalledWith(expect.stringContaining("no origin")); + }); + + it("calls logger.warning when the extension URL cannot be determined", () => { + jest.spyOn(BrowserApi, "getRuntimeURL").mockReturnValue(""); + const logger = mock(); + + BrowserApi.senderIsInternal({ origin: EXTENSION_ORIGIN }, logger); + + expect(logger.warning).toHaveBeenCalledWith(expect.stringContaining("extension URL")); + }); + + it("calls logger.warning when origin does not match", () => { + const logger = mock(); + + BrowserApi.senderIsInternal({ origin: "https://evil.com" }, logger); + + expect(logger.warning).toHaveBeenCalledWith(expect.stringContaining("does not match")); + }); + + it("calls logger.warning when sender is from a non-top-level frame", () => { + const logger = mock(); + + BrowserApi.senderIsInternal({ origin: EXTENSION_ORIGIN, frameId: 5 }, logger); + + expect(logger.warning).toHaveBeenCalledWith(expect.stringContaining("top-level frame")); + }); + + it("calls logger.info when sender is confirmed internal", () => { + const logger = mock(); + + BrowserApi.senderIsInternal({ origin: EXTENSION_ORIGIN }, logger); + + expect(logger.info).toHaveBeenCalledWith(expect.stringContaining("internal")); + }); + }); + describe("getWindow", () => { it("will get the current window if a window id is not provided", () => { // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. diff --git a/apps/browser/src/platform/browser/browser-api.ts b/apps/browser/src/platform/browser/browser-api.ts index feefd527636..1b0f7639d1d 100644 --- a/apps/browser/src/platform/browser/browser-api.ts +++ b/apps/browser/src/platform/browser/browser-api.ts @@ -6,7 +6,7 @@ import { BrowserClientVendors } from "@bitwarden/common/autofill/constants"; import { BrowserClientVendor } from "@bitwarden/common/autofill/types"; import { DeviceType } from "@bitwarden/common/enums"; import { LogService } from "@bitwarden/logging"; -import { isBrowserSafariApi } from "@bitwarden/platform"; +import { isBrowserSafariApi, urlOriginsMatch } from "@bitwarden/platform"; import { TabMessage } from "../../types/tab-messages"; import { BrowserPlatformUtilsService } from "../services/platform-utils/browser-platform-utils.service"; @@ -34,12 +34,20 @@ export class BrowserApi { } /** - * Helper method that attempts to distinguish whether a message sender is internal to the extension or not. + * Returns `true` if the message sender appears to originate from within this extension. * - * Currently this is done through source origin matching, and frameId checking (only top-level frames are internal). - * @param sender a message sender - * @param logger an optional logger to log validation results - * @returns whether or not the sender appears to be internal to the extension + * Returns `false` when: + * - `sender` is absent or has no `origin` property + * - The extension's own URL cannot be determined at runtime + * - The sender's origin does not match the extension's origin (compared by scheme, host, and port; + * senders without a host such as `file:` or `data:` URLs are always rejected) + * - The message comes from a sub-frame rather than the top-level frame + * + * Note: this is a best-effort check that relies on the browser correctly populating `sender.origin`. + * + * @param sender - The message sender to validate. `undefined` or a sender without `origin` returns `false`. + * @param logger - Optional logger; rejections are reported at `warning` level, acceptance at `info`. + * @returns `true` if the sender appears to be internal to the extension; `false` otherwise. */ static senderIsInternal( sender: chrome.runtime.MessageSender | undefined, @@ -49,28 +57,22 @@ export class BrowserApi { logger?.warning("[BrowserApi] Message sender has no origin"); return false; } - const extensionUrl = - (typeof chrome !== "undefined" && chrome.runtime?.getURL("")) || - (typeof browser !== "undefined" && browser.runtime?.getURL("")) || - ""; + // Empty path yields the extension's base URL; coalesce to empty string so the guard below fires on a missing runtime. + const extensionUrl = BrowserApi.getRuntimeURL("") ?? ""; if (!extensionUrl) { logger?.warning("[BrowserApi] Unable to determine extension URL"); return false; } - // Normalize both URLs by removing trailing slashes - const normalizedOrigin = sender.origin.replace(/\/$/, "").toLowerCase(); - const normalizedExtensionUrl = extensionUrl.replace(/\/$/, "").toLowerCase(); - - if (!normalizedOrigin.startsWith(normalizedExtensionUrl)) { + if (!urlOriginsMatch(extensionUrl, sender.origin)) { logger?.warning( - `[BrowserApi] Message sender origin (${normalizedOrigin}) does not match extension URL (${normalizedExtensionUrl})`, + `[BrowserApi] Message sender origin (${sender.origin}) does not match extension URL (${extensionUrl})`, ); return false; } - // We only send messages from the top-level frame, but frameId is only set if tab is set, which for popups it is not. + // frameId is absent for popups, so use an 'in' check rather than direct comparison. if ("frameId" in sender && sender.frameId !== 0) { logger?.warning("[BrowserApi] Message sender is not from the top-level frame"); return false; diff --git a/libs/platform/src/index.ts b/libs/platform/src/index.ts index 3fabe3fad1a..9c9dac0c684 100644 --- a/libs/platform/src/index.ts +++ b/libs/platform/src/index.ts @@ -1,2 +1,3 @@ export * from "./services/browser-service"; export * from "./background-sync"; +export * from "./util"; diff --git a/libs/platform/src/util.spec.ts b/libs/platform/src/util.spec.ts new file mode 100644 index 00000000000..fda563db7ea --- /dev/null +++ b/libs/platform/src/util.spec.ts @@ -0,0 +1,54 @@ +import { urlOriginsMatch } from "./util"; + +describe("urlOriginsMatch", () => { + it.each([ + ["string/string, same origin", "https://example.com", "https://example.com"], + ["URL/URL, same origin", new URL("https://example.com"), new URL("https://example.com")], + ["string canonical, URL suspect", "https://example.com", new URL("https://example.com/path")], + ["URL canonical, string suspect", new URL("https://example.com/path"), "https://example.com"], + [ + "paths and query differ but origin same", + "https://example.com/foo", + "https://example.com/bar?baz=1", + ], + ["explicit default port matches implicit", "https://example.com", "https://example.com:443"], + [ + "non-special scheme with matching host", + "chrome-extension://abc123/popup.html", + "chrome-extension://abc123/bg.js", + ], + ])("returns true when %s", (_, canonical, suspect) => { + expect(urlOriginsMatch(canonical as string | URL, suspect as string | URL)).toBe(true); + }); + + it.each([ + ["hosts differ", "https://example.com", "https://evil.com"], + ["schemes differ", "https://example.com", "http://example.com"], + ["ports differ", "https://example.com:8080", "https://example.com:9090"], + [ + "suspect is a subdomain of the canonical host", + "https://example.com", + "https://sub.example.com", + ], + ["non-special scheme hosts differ", "chrome-extension://abc123/", "chrome-extension://xyz789/"], + ])("returns false when %s", (_, canonical, suspect) => { + expect(urlOriginsMatch(canonical, suspect)).toBe(false); + }); + + it.each([ + ["canonical is an invalid string", "not a url", "https://example.com"], + ["suspect is an invalid string", "https://example.com", "not a url"], + ])("returns false when %s", (_, canonical, suspect) => { + expect(urlOriginsMatch(canonical, suspect)).toBe(false); + }); + + it.each([ + ["canonical is a file: URL", "file:///home/user/a.txt", "https://example.com"], + ["suspect is a file: URL", "https://example.com", "file:///home/user/a.txt"], + ["both are file: URLs", "file:///home/user/a.txt", "file:///home/other/b.txt"], + ["canonical is a data: URL", "data:text/plain,foo", "https://example.com"], + ["suspect is a data: URL", "https://example.com", "data:text/plain,foo"], + ])("returns false when %s (no host)", (_, canonical, suspect) => { + expect(urlOriginsMatch(canonical, suspect)).toBe(false); + }); +}); diff --git a/libs/platform/src/util.ts b/libs/platform/src/util.ts new file mode 100644 index 00000000000..b59e713fba3 --- /dev/null +++ b/libs/platform/src/util.ts @@ -0,0 +1,53 @@ +function toURL(input: string | URL): URL | null { + if (input instanceof URL) { + return input; + } + try { + return new URL(input); + } catch { + return null; + } +} + +function effectiveOrigin(url: URL): string | null { + // The URL spec returns "null" for .origin on non-special schemes + // (e.g. chrome-extension://) so we build the origin from protocol + host instead. + // An empty host means no meaningful origin can be compared (file:, data:, etc.). + if (!url.host) { + return null; + } + return `${url.protocol}//${url.host}`; +} + +/** + * Compares two URLs to determine whether the suspect URL originates from the + * same host as the canonical URL. + * + * Both arguments accept either a string or an existing {@link URL} object. + * + * Returns `false` when: + * - Either argument cannot be parsed as a valid URL + * - Either URL has no host (e.g. `file:`, `data:` schemes), since URLs without + * a meaningful host cannot be distinguished by origin + * + * @param canonical - The reference URL whose origin acts as the baseline. + * @param suspect - The URL being tested against the canonical origin. + * @returns `true` if both URLs share the same scheme, host, and port; `false` otherwise. + */ +export function urlOriginsMatch(canonical: string | URL, suspect: string | URL): boolean { + const canonicalUrl = toURL(canonical); + const suspectUrl = toURL(suspect); + + if (!canonicalUrl || !suspectUrl) { + return false; + } + + const canonicalOrigin = effectiveOrigin(canonicalUrl); + const suspectOrigin = effectiveOrigin(suspectUrl); + + if (!canonicalOrigin || !suspectOrigin) { + return false; + } + + return canonicalOrigin === suspectOrigin; +} From c8ba23e28df4ae76c601a21eedeb985e8642bf32 Mon Sep 17 00:00:00 2001 From: Brandon Treston Date: Thu, 19 Feb 2026 09:57:52 -0500 Subject: [PATCH 28/89] [PM-26378] Auto confirm events (#19025) * add notification handler for auto confirm * add missing state check * fix test * isolate angular specific code from shared lib code * clean up * use autoconfirm method * add event logging for auto confirm * update copy --- .../settings/admin-settings.component.spec.ts | 13 +++++++++ .../settings/admin-settings.component.ts | 27 +++++++++++++++---- .../organizations/manage/events.component.ts | 1 + apps/web/src/app/core/event.service.ts | 19 +++++++++++++ apps/web/src/locales/en/messages.json | 24 +++++++++++++++++ .../default-auto-confirm.service.spec.ts | 15 +++++++++++ .../src/enums/event-system-user.enum.ts | 1 + libs/common/src/enums/event-type.enum.ts | 5 ++++ 8 files changed, 100 insertions(+), 5 deletions(-) diff --git a/apps/browser/src/vault/popup/settings/admin-settings.component.spec.ts b/apps/browser/src/vault/popup/settings/admin-settings.component.spec.ts index f7b4e7b473a..2a9ebdcddf6 100644 --- a/apps/browser/src/vault/popup/settings/admin-settings.component.spec.ts +++ b/apps/browser/src/vault/popup/settings/admin-settings.component.spec.ts @@ -7,6 +7,8 @@ import { of } from "rxjs"; import { NudgesService, NudgeType } from "@bitwarden/angular/vault"; import { AutoConfirmState, AutomaticUserConfirmationService } from "@bitwarden/auto-confirm"; import { PopOutComponent } from "@bitwarden/browser/platform/popup/components/pop-out.component"; +import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; +import { InternalOrganizationServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { mockAccountServiceWith } from "@bitwarden/common/spec"; @@ -52,6 +54,8 @@ describe("AdminSettingsComponent", () => { let autoConfirmService: MockProxy; let nudgesService: MockProxy; let mockDialogService: MockProxy; + let eventCollectionService: MockProxy; + let organizationService: MockProxy; const userId = "test-user-id" as UserId; const mockAutoConfirmState: AutoConfirmState = { @@ -64,10 +68,14 @@ describe("AdminSettingsComponent", () => { autoConfirmService = mock(); nudgesService = mock(); mockDialogService = mock(); + eventCollectionService = mock(); + organizationService = mock(); autoConfirmService.configuration$.mockReturnValue(of(mockAutoConfirmState)); autoConfirmService.upsert.mockResolvedValue(undefined); nudgesService.showNudgeSpotlight$.mockReturnValue(of(false)); + eventCollectionService.collect.mockResolvedValue(undefined); + organizationService.organizations$.mockReturnValue(of([])); await TestBed.configureTestingModule({ imports: [AdminSettingsComponent], @@ -77,6 +85,11 @@ describe("AdminSettingsComponent", () => { { provide: AutomaticUserConfirmationService, useValue: autoConfirmService }, { provide: DialogService, useValue: mockDialogService }, { provide: NudgesService, useValue: nudgesService }, + { provide: EventCollectionService, useValue: eventCollectionService }, + { + provide: InternalOrganizationServiceAbstraction, + useValue: organizationService, + }, { provide: I18nService, useValue: { t: (key: string) => key } }, ], }) diff --git a/apps/browser/src/vault/popup/settings/admin-settings.component.ts b/apps/browser/src/vault/popup/settings/admin-settings.component.ts index 52da4318047..99cb5a814c1 100644 --- a/apps/browser/src/vault/popup/settings/admin-settings.component.ts +++ b/apps/browser/src/vault/popup/settings/admin-settings.component.ts @@ -20,8 +20,11 @@ import { import { PopOutComponent } from "@bitwarden/browser/platform/popup/components/pop-out.component"; import { PopupHeaderComponent } from "@bitwarden/browser/platform/popup/layout/popup-header.component"; import { PopupPageComponent } from "@bitwarden/browser/platform/popup/layout/popup-page.component"; +import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; +import { InternalOrganizationServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; +import { EventType } from "@bitwarden/common/enums"; import { BitIconButtonComponent, CardComponent, @@ -69,6 +72,8 @@ export class AdminSettingsComponent implements OnInit { private destroyRef: DestroyRef, private dialogService: DialogService, private nudgesService: NudgesService, + private eventCollectionService: EventCollectionService, + private organizationService: InternalOrganizationServiceAbstraction, ) {} async ngOnInit() { @@ -88,14 +93,26 @@ export class AdminSettingsComponent implements OnInit { } return of(false); }), - withLatestFrom(this.autoConfirmService.configuration$(userId)), - switchMap(([newValue, existingState]) => - this.autoConfirmService.upsert(userId, { + withLatestFrom( + this.autoConfirmService.configuration$(userId), + this.organizationService.organizations$(userId), + ), + switchMap(async ([newValue, existingState, organizations]) => { + await this.autoConfirmService.upsert(userId, { ...existingState, enabled: newValue, showBrowserNotification: false, - }), - ), + }); + + // Auto-confirm users can only belong to one organization + const organization = organizations[0]; + if (organization?.id) { + const eventType = newValue + ? EventType.Organization_AutoConfirmEnabled_Admin + : EventType.Organization_AutoConfirmDisabled_Admin; + await this.eventCollectionService.collect(eventType, undefined, true, organization.id); + } + }), takeUntilDestroyed(this.destroyRef), ) .subscribe(); diff --git a/apps/web/src/app/admin-console/organizations/manage/events.component.ts b/apps/web/src/app/admin-console/organizations/manage/events.component.ts index 62f6539cc16..fffe1c06ab8 100644 --- a/apps/web/src/app/admin-console/organizations/manage/events.component.ts +++ b/apps/web/src/app/admin-console/organizations/manage/events.component.ts @@ -44,6 +44,7 @@ const EVENT_SYSTEM_USER_TO_TRANSLATION: Record = { [EventSystemUser.SCIM]: null, // SCIM acronym not able to be translated so just display SCIM [EventSystemUser.DomainVerification]: "domainVerification", [EventSystemUser.PublicApi]: "publicApi", + [EventSystemUser.BitwardenPortal]: "system", }; // FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush diff --git a/apps/web/src/app/core/event.service.ts b/apps/web/src/app/core/event.service.ts index 47f4344ec36..006014b9fed 100644 --- a/apps/web/src/app/core/event.service.ts +++ b/apps/web/src/app/core/event.service.ts @@ -355,6 +355,13 @@ export class EventService { this.getShortId(ev.organizationUserId), ); break; + case EventType.OrganizationUser_AutomaticallyConfirmed: + msg = this.i18nService.t("automaticallyConfirmedUserId", this.formatOrgUserId(ev)); + humanReadableMsg = this.i18nService.t( + "automaticallyConfirmedUserId", + this.getShortId(ev.organizationUserId), + ); + break; // Org case EventType.Organization_Updated: msg = humanReadableMsg = this.i18nService.t("editedOrgSettings"); @@ -458,6 +465,18 @@ export class EventService { case EventType.Organization_ItemOrganization_Declined: msg = humanReadableMsg = this.i18nService.t("userDeclinedTransfer"); break; + case EventType.Organization_AutoConfirmEnabled_Admin: + msg = humanReadableMsg = this.i18nService.t("autoConfirmEnabledByAdmin"); + break; + case EventType.Organization_AutoConfirmDisabled_Admin: + msg = humanReadableMsg = this.i18nService.t("autoConfirmDisabledByAdmin"); + break; + case EventType.Organization_AutoConfirmEnabled_Portal: + msg = humanReadableMsg = this.i18nService.t("autoConfirmEnabledByPortal"); + break; + case EventType.Organization_AutoConfirmDisabled_Portal: + msg = humanReadableMsg = this.i18nService.t("autoConfirmDisabledByPortal"); + break; // Policies case EventType.Policy_Updated: { diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index b257a68052d..c45d7e5d630 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Edited user $ID$.", "placeholders": { @@ -6142,6 +6151,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Remove individual vault" }, diff --git a/libs/auto-confirm/src/services/default-auto-confirm.service.spec.ts b/libs/auto-confirm/src/services/default-auto-confirm.service.spec.ts index 0ea3ca9c23a..2b098d3c231 100644 --- a/libs/auto-confirm/src/services/default-auto-confirm.service.spec.ts +++ b/libs/auto-confirm/src/services/default-auto-confirm.service.spec.ts @@ -439,6 +439,21 @@ describe("DefaultAutomaticUserConfirmationService", () => { expect(organizationUserApiService.postOrganizationUserAutoConfirm).not.toHaveBeenCalled(); }); + it("should return early when auto-confirm is disabled in configuration", async () => { + const disabledConfig = new AutoConfirmState(); + disabledConfig.enabled = false; + await stateProvider.setUserState( + AUTO_CONFIRM_STATE, + { [mockUserId]: disabledConfig }, + mockUserId, + ); + + await service.autoConfirmUser(mockUserId, mockConfirmingUserId, mockOrganizationId); + + expect(apiService.getUserPublicKey).not.toHaveBeenCalled(); + expect(organizationUserApiService.postOrganizationUserAutoConfirm).not.toHaveBeenCalled(); + }); + it("should build confirm request with organization and public key", async () => { await service.autoConfirmUser(mockUserId, mockConfirmingUserId, mockOrganizationId); diff --git a/libs/common/src/enums/event-system-user.enum.ts b/libs/common/src/enums/event-system-user.enum.ts index f4abbb1e3e9..e5e92ee7ef1 100644 --- a/libs/common/src/enums/event-system-user.enum.ts +++ b/libs/common/src/enums/event-system-user.enum.ts @@ -5,4 +5,5 @@ export enum EventSystemUser { SCIM = 1, DomainVerification = 2, PublicApi = 3, + BitwardenPortal = 5, } diff --git a/libs/common/src/enums/event-type.enum.ts b/libs/common/src/enums/event-type.enum.ts index 4750c881f06..e1bda61b111 100644 --- a/libs/common/src/enums/event-type.enum.ts +++ b/libs/common/src/enums/event-type.enum.ts @@ -60,6 +60,7 @@ export enum EventType { OrganizationUser_RejectedAuthRequest = 1514, OrganizationUser_Deleted = 1515, OrganizationUser_Left = 1516, + OrganizationUser_AutomaticallyConfirmed = 1517, Organization_Updated = 1600, Organization_PurgedVault = 1601, @@ -81,6 +82,10 @@ export enum EventType { Organization_CollectionManagement_AllowAdminAccessToAllCollectionItemsDisabled = 1617, Organization_ItemOrganization_Accepted = 1618, Organization_ItemOrganization_Declined = 1619, + Organization_AutoConfirmEnabled_Admin = 1620, + Organization_AutoConfirmDisabled_Admin = 1621, + Organization_AutoConfirmEnabled_Portal = 1622, + Organization_AutoConfirmDisabled_Portal = 1623, Policy_Updated = 1700, From 4f256fee6d79a0d30f7bcc0cded8431047442437 Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Thu, 19 Feb 2026 09:26:18 -0600 Subject: [PATCH 29/89] [PM-29087] [PM-29088] Remove FF: `pm-26793-fetch-premium-price-from-pricing-service` - Logic + Flag (#18946) * refactor(billing): remove PM-26793 feature flag from subscription pricing service * test(billing): update subscription pricing tests for PM-26793 feature flag removal * chore: remove PM-26793 feature flag from keys --- .../subscription-pricing.service.spec.ts | 72 +++---------------- .../services/subscription-pricing.service.ts | 70 ++++++------------ libs/common/src/enums/feature-flag.enum.ts | 2 - 3 files changed, 30 insertions(+), 114 deletions(-) diff --git a/libs/common/src/billing/services/subscription-pricing.service.spec.ts b/libs/common/src/billing/services/subscription-pricing.service.spec.ts index 33bfcebeb58..e96c0d4b74c 100644 --- a/libs/common/src/billing/services/subscription-pricing.service.spec.ts +++ b/libs/common/src/billing/services/subscription-pricing.service.spec.ts @@ -231,6 +231,7 @@ describe("DefaultSubscriptionPricingService", () => { }, storage: { price: 4, + provided: 1, }, } as PremiumPlanResponse; @@ -350,7 +351,7 @@ describe("DefaultSubscriptionPricingService", () => { billingApiService.getPlans.mockResolvedValue(mockPlansResponse); billingApiService.getPremiumPlan.mockResolvedValue(mockPremiumPlanResponse); - configService.getFeatureFlag$.mockReturnValue(of(false)); // Default to false (use hardcoded value) + configService.getFeatureFlag$.mockReturnValue(of(false)); setupEnvironmentService(environmentService); service = new DefaultSubscriptionPricingService( @@ -915,7 +916,7 @@ describe("DefaultSubscriptionPricingService", () => { const testError = new Error("Premium plan API error"); errorBillingApiService.getPlans.mockResolvedValue(mockPlansResponse); errorBillingApiService.getPremiumPlan.mockRejectedValue(testError); - errorConfigService.getFeatureFlag$.mockReturnValue(of(true)); // Enable feature flag to use premium plan API + errorConfigService.getFeatureFlag$.mockReturnValue(of(false)); setupEnvironmentService(errorEnvironmentService); const errorService = new DefaultSubscriptionPricingService( @@ -959,71 +960,16 @@ describe("DefaultSubscriptionPricingService", () => { expect(getPlansResponse).toHaveBeenCalledTimes(1); }); - it("should share premium plan API response between multiple subscriptions when feature flag is enabled", () => { - // Create a new mock to avoid conflicts with beforeEach setup - const newBillingApiService = mock(); - const newConfigService = mock(); - const newEnvironmentService = mock(); - - newBillingApiService.getPlans.mockResolvedValue(mockPlansResponse); - newBillingApiService.getPremiumPlan.mockResolvedValue(mockPremiumPlanResponse); - newConfigService.getFeatureFlag$.mockReturnValue(of(true)); - setupEnvironmentService(newEnvironmentService); - - const getPremiumPlanSpy = jest.spyOn(newBillingApiService, "getPremiumPlan"); - - // Create a new service instance with the feature flag enabled - const newService = new DefaultSubscriptionPricingService( - newBillingApiService, - newConfigService, - i18nService, - logService, - newEnvironmentService, - ); + it("should share premium plan API response between multiple subscriptions", () => { + const getPremiumPlanSpy = jest.spyOn(billingApiService, "getPremiumPlan"); // Subscribe to the premium pricing tier multiple times - newService.getPersonalSubscriptionPricingTiers$().subscribe(); - newService.getPersonalSubscriptionPricingTiers$().subscribe(); + service.getPersonalSubscriptionPricingTiers$().subscribe(); + service.getPersonalSubscriptionPricingTiers$().subscribe(); // API should only be called once due to shareReplay on premiumPlanResponse$ expect(getPremiumPlanSpy).toHaveBeenCalledTimes(1); }); - - it("should use hardcoded premium price when feature flag is disabled", (done) => { - // Create a new mock to test from scratch - const newBillingApiService = mock(); - const newConfigService = mock(); - const newEnvironmentService = mock(); - - newBillingApiService.getPlans.mockResolvedValue(mockPlansResponse); - newBillingApiService.getPremiumPlan.mockResolvedValue({ - seat: { price: 999 }, // Different price to verify hardcoded value is used - storage: { price: 999 }, - } as PremiumPlanResponse); - newConfigService.getFeatureFlag$.mockReturnValue(of(false)); - setupEnvironmentService(newEnvironmentService); - - // Create a new service instance with the feature flag disabled - const newService = new DefaultSubscriptionPricingService( - newBillingApiService, - newConfigService, - i18nService, - logService, - newEnvironmentService, - ); - - // Subscribe with feature flag disabled - newService.getPersonalSubscriptionPricingTiers$().subscribe((tiers) => { - const premiumTier = tiers.find( - (tier) => tier.id === PersonalSubscriptionPricingTierIds.Premium, - ); - - // Should use hardcoded value of 10, not the API response value of 999 - expect(premiumTier!.passwordManager.annualPrice).toBe(10); - expect(premiumTier!.passwordManager.annualPricePerAdditionalStorageGB).toBe(4); - done(); - }); - }); }); describe("Self-hosted environment behavior", () => { @@ -1035,7 +981,7 @@ describe("DefaultSubscriptionPricingService", () => { const getPlansSpy = jest.spyOn(selfHostedBillingApiService, "getPlans"); const getPremiumPlanSpy = jest.spyOn(selfHostedBillingApiService, "getPremiumPlan"); - selfHostedConfigService.getFeatureFlag$.mockReturnValue(of(true)); + selfHostedConfigService.getFeatureFlag$.mockReturnValue(of(false)); setupEnvironmentService(selfHostedEnvironmentService, Region.SelfHosted); const selfHostedService = new DefaultSubscriptionPricingService( @@ -1061,7 +1007,7 @@ describe("DefaultSubscriptionPricingService", () => { const selfHostedConfigService = mock(); const selfHostedEnvironmentService = mock(); - selfHostedConfigService.getFeatureFlag$.mockReturnValue(of(true)); + selfHostedConfigService.getFeatureFlag$.mockReturnValue(of(false)); setupEnvironmentService(selfHostedEnvironmentService, Region.SelfHosted); const selfHostedService = new DefaultSubscriptionPricingService( diff --git a/libs/common/src/billing/services/subscription-pricing.service.ts b/libs/common/src/billing/services/subscription-pricing.service.ts index 9ba76d348d0..73f3dc1bc77 100644 --- a/libs/common/src/billing/services/subscription-pricing.service.ts +++ b/libs/common/src/billing/services/subscription-pricing.service.ts @@ -33,16 +33,6 @@ import { } from "../types/subscription-pricing-tier"; export class DefaultSubscriptionPricingService implements SubscriptionPricingServiceAbstraction { - /** - * Fallback premium pricing used when the feature flag is disabled. - * These values represent the legacy pricing model and will not reflect - * server-side price changes. They are retained for backward compatibility - * during the feature flag rollout period. - */ - private static readonly FALLBACK_PREMIUM_SEAT_PRICE = 10; - private static readonly FALLBACK_PREMIUM_STORAGE_PRICE = 4; - private static readonly FALLBACK_PREMIUM_PROVIDED_STORAGE_GB = 1; - constructor( private billingApiService: BillingApiServiceAbstraction, private configService: ConfigService, @@ -123,45 +113,27 @@ export class DefaultSubscriptionPricingService implements SubscriptionPricingSer shareReplay({ bufferSize: 1, refCount: false }), ); - private premium$: Observable = this.configService - .getFeatureFlag$(FeatureFlag.PM26793_FetchPremiumPriceFromPricingService) - .pipe( - take(1), // Lock behavior at first subscription to prevent switching data sources mid-stream - switchMap((fetchPremiumFromPricingService) => - fetchPremiumFromPricingService - ? this.premiumPlanResponse$.pipe( - map((premiumPlan) => ({ - seat: premiumPlan.seat?.price, - storage: premiumPlan.storage?.price, - provided: premiumPlan.storage?.provided, - })), - ) - : of({ - seat: DefaultSubscriptionPricingService.FALLBACK_PREMIUM_SEAT_PRICE, - storage: DefaultSubscriptionPricingService.FALLBACK_PREMIUM_STORAGE_PRICE, - provided: DefaultSubscriptionPricingService.FALLBACK_PREMIUM_PROVIDED_STORAGE_GB, - }), - ), - map((premiumPrices) => ({ - id: PersonalSubscriptionPricingTierIds.Premium, - name: this.i18nService.t("premium"), - description: this.i18nService.t("advancedOnlineSecurity"), - availableCadences: [SubscriptionCadenceIds.Annually], - passwordManager: { - type: "standalone", - annualPrice: premiumPrices.seat, - annualPricePerAdditionalStorageGB: premiumPrices.storage, - providedStorageGB: premiumPrices.provided, - features: [ - this.featureTranslations.builtInAuthenticator(), - this.featureTranslations.secureFileStorage(), - this.featureTranslations.emergencyAccess(), - this.featureTranslations.breachMonitoring(), - this.featureTranslations.andMoreFeatures(), - ], - }, - })), - ); + private premium$: Observable = this.premiumPlanResponse$.pipe( + map((premiumPlan) => ({ + id: PersonalSubscriptionPricingTierIds.Premium, + name: this.i18nService.t("premium"), + description: this.i18nService.t("advancedOnlineSecurity"), + availableCadences: [SubscriptionCadenceIds.Annually], + passwordManager: { + type: "standalone", + annualPrice: premiumPlan.seat?.price, + annualPricePerAdditionalStorageGB: premiumPlan.storage?.price, + providedStorageGB: premiumPlan.storage?.provided, + features: [ + this.featureTranslations.builtInAuthenticator(), + this.featureTranslations.secureFileStorage(), + this.featureTranslations.emergencyAccess(), + this.featureTranslations.breachMonitoring(), + this.featureTranslations.andMoreFeatures(), + ], + }, + })), + ); private families$: Observable = this.organizationPlansResponse$.pipe( diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index 5160e6aa542..b7fad43ebbf 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -31,7 +31,6 @@ export enum FeatureFlag { /* Billing */ TrialPaymentOptional = "PM-8163-trial-payment", PM24032_NewNavigationPremiumUpgradeButton = "pm-24032-new-navigation-premium-upgrade-button", - PM26793_FetchPremiumPriceFromPricingService = "pm-26793-fetch-premium-price-from-pricing-service", PM23713_PremiumBadgeOpensNewPremiumUpgradeDialog = "pm-23713-premium-badge-opens-new-premium-upgrade-dialog", PM26462_Milestone_3 = "pm-26462-milestone-3", PM23341_Milestone_2 = "pm-23341-milestone-2", @@ -146,7 +145,6 @@ export const DefaultFeatureFlagValue = { /* Billing */ [FeatureFlag.TrialPaymentOptional]: FALSE, [FeatureFlag.PM24032_NewNavigationPremiumUpgradeButton]: FALSE, - [FeatureFlag.PM26793_FetchPremiumPriceFromPricingService]: FALSE, [FeatureFlag.PM23713_PremiumBadgeOpensNewPremiumUpgradeDialog]: FALSE, [FeatureFlag.PM26462_Milestone_3]: FALSE, [FeatureFlag.PM23341_Milestone_2]: FALSE, From d0ccb9cd31b0bb8cfad93dc0470dbdfb9252df96 Mon Sep 17 00:00:00 2001 From: Vijay Oommen Date: Thu, 19 Feb 2026 11:12:03 -0600 Subject: [PATCH 30/89] [PM-32013] Empty state incorrectly rendered (#19033) --- .../applications.component.html | 4 +- .../applications.component.ts | 30 ++-- .../chip-select/chip-select.component.spec.ts | 156 +++++++++++++++++- .../src/chip-select/chip-select.component.ts | 11 ++ 4 files changed, 179 insertions(+), 22 deletions(-) diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications/applications.component.html b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications/applications.component.html index 27864fa2f87..743f8ff1b68 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications/applications.component.html +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications/applications.component.html @@ -84,9 +84,9 @@ class="tw-mb-10" > - @if (emptyTableExplanation()) { + @if (this.dataSource.filteredData?.length === 0) {
- {{ emptyTableExplanation() }} + {{ "noApplicationsMatchTheseFilters" | i18n }}
}
diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications/applications.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications/applications.component.ts index 0020106ba7d..962628584d3 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications/applications.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications/applications.component.ts @@ -117,7 +117,6 @@ export class ApplicationsComponent implements OnInit { icon: " ", }, ]); - protected readonly emptyTableExplanation = signal(""); readonly allSelectedAppsAreCritical = computed(() => { if (!this.dataSource.filteredData || this.selectedUrls().size == 0) { @@ -174,6 +173,9 @@ export class ApplicationsComponent implements OnInit { })); this.dataSource.data = tableDataWithIcon; this.totalApplicationsCount.set(report.reportData.length); + this.criticalApplicationsCount.set( + report.reportData.filter((app) => app.isMarkedAsCritical).length, + ); } else { this.dataSource.data = []; } @@ -183,16 +185,6 @@ export class ApplicationsComponent implements OnInit { }, }); - this.dataService.criticalReportResults$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe({ - next: (criticalReport) => { - if (criticalReport != null) { - this.criticalApplicationsCount.set(criticalReport.reportData.length); - } else { - this.criticalApplicationsCount.set(0); - } - }, - }); - combineLatest([ this.searchControl.valueChanges.pipe(startWith("")), this.selectedFilterObservable, @@ -219,12 +211,6 @@ export class ApplicationsComponent implements OnInit { } }); this.selectedUrls.set(filteredUrls); - - if (this.dataSource?.filteredData?.length === 0) { - this.emptyTableExplanation.set(this.i18nService.t("noApplicationsMatchTheseFilters")); - } else { - this.emptyTableExplanation.set(""); - } }); } @@ -240,7 +226,7 @@ export class ApplicationsComponent implements OnInit { .saveCriticalApplications(Array.from(this.selectedUrls())) .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe({ - next: () => { + next: (response) => { this.toastService.showToast({ variant: "success", title: "", @@ -248,6 +234,9 @@ export class ApplicationsComponent implements OnInit { }); this.selectedUrls.set(new Set()); this.updatingCriticalApps.set(false); + this.criticalApplicationsCount.set( + response?.data?.summaryData?.totalCriticalApplicationCount ?? 0, + ); }, error: () => { this.toastService.showToast({ @@ -267,7 +256,7 @@ export class ApplicationsComponent implements OnInit { .removeCriticalApplications(appsToUnmark) .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe({ - next: () => { + next: (response) => { this.toastService.showToast({ message: this.i18nService.t( "numApplicationsUnmarkedCriticalSuccess", @@ -277,6 +266,9 @@ export class ApplicationsComponent implements OnInit { }); this.selectedUrls.set(new Set()); this.updatingCriticalApps.set(false); + this.criticalApplicationsCount.set( + response?.data?.summaryData?.totalCriticalApplicationCount ?? 0, + ); }, error: () => { this.toastService.showToast({ diff --git a/libs/components/src/chip-select/chip-select.component.spec.ts b/libs/components/src/chip-select/chip-select.component.spec.ts index 3a66b799652..bfabb5ea95e 100644 --- a/libs/components/src/chip-select/chip-select.component.spec.ts +++ b/libs/components/src/chip-select/chip-select.component.spec.ts @@ -1,4 +1,4 @@ -import { ChangeDetectionStrategy, Component, signal } from "@angular/core"; +import { ChangeDetectionStrategy, Component, signal, computed } from "@angular/core"; import { ComponentFixture, TestBed } from "@angular/core/testing"; import { FormControl } from "@angular/forms"; import { By } from "@angular/platform-browser"; @@ -502,3 +502,157 @@ class TestAppComponent { readonly disabled = signal(false); readonly fullWidth = signal(false); } + +describe("ChipSelectComponentWithDynamicOptions", () => { + let component: ChipSelectComponent; + let fixture: ComponentFixture; + + const getChipButton = () => + fixture.debugElement.query(By.css("[data-fvw-target]"))?.nativeElement as HTMLButtonElement; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [TestAppWithDynamicOptionsComponent, NoopAnimationsModule], + providers: [{ provide: I18nService, useValue: mockI18nService }], + }).compileComponents(); + + fixture = TestBed.createComponent(TestAppWithDynamicOptionsComponent); + fixture.detectChanges(); + + component = fixture.debugElement.query(By.directive(ChipSelectComponent)).componentInstance; + + fixture.componentInstance.firstCounter.set(0); + fixture.componentInstance.secondCounter.set(0); + + fixture.detectChanges(); + }); + + describe("User-Facing Behavior", () => { + it("should update available options when they change", () => { + const first = 5; + const second = 10; + + const testApp = fixture.componentInstance; + testApp.firstCounter.set(first); + testApp.secondCounter.set(second); + fixture.detectChanges(); + + getChipButton().click(); + fixture.detectChanges(); + + const menuItems = Array.from(document.querySelectorAll("[bitMenuItem]")); + expect(menuItems.some((el) => el.textContent?.includes(`Option - ${first}`))).toBe(true); + expect(menuItems.some((el) => el.textContent?.includes(`Option - ${second}`))).toBe(true); + }); + }); + + describe("Form Integration Behavior", () => { + it("should display selected option when form control value is set", () => { + const testApp = fixture.componentInstance; + testApp.firstCounter.set(1); + testApp.secondCounter.set(2); + + component.writeValue("opt2"); // select second menu option which has dynamic label + fixture.detectChanges(); + + const button = getChipButton(); + expect(button.textContent?.trim()).toContain("Option - 2"); // verify that the label reflects the dynamic value + + // change the dynamic values and verify that the menu still shows the correct labels for the options + // it should also keep opt2 selected since it's the same value, just with an updated label + const first = 10; + const second = 20; + + testApp.firstCounter.set(first); + testApp.secondCounter.set(second); + fixture.detectChanges(); + + // again, verify that the label reflects the dynamic value + expect(button.textContent?.trim()).toContain(`Option - ${second}`); + + // click the button to open the menu + getChipButton().click(); + fixture.detectChanges(); + + // verify that the menu items also reflect the updated dynamic values + const menuItems = Array.from(document.querySelectorAll("[bitMenuItem]")); + expect(menuItems.some((el) => el.textContent?.includes(`Option - ${first}`))).toBe(true); + expect(menuItems.some((el) => el.textContent?.includes(`Option - ${second}`))).toBe(true); + }); + + it("should find and display nested option when form control value is set", () => { + const testApp = fixture.componentInstance; + testApp.firstCounter.set(1); + testApp.secondCounter.set(2); + + component.writeValue("child1"); // select a child menu item + fixture.detectChanges(); + + const button = getChipButton(); + // verify that the label reflects the dynamic value for the child option + expect(button.textContent?.trim()).toContain("Child - 1"); + + const first = 10; + const second = 20; + + testApp.firstCounter.set(first); + testApp.secondCounter.set(second); + fixture.detectChanges(); + + // again, verify that the label reflects the dynamic value + expect(button.textContent?.trim()).toContain(`Child - ${first}`); + }); + + it("should clear selection when form control value is set to null", () => { + const testApp = fixture.componentInstance; + testApp.firstCounter.set(1); + testApp.secondCounter.set(2); + + component.writeValue("opt1"); + fixture.detectChanges(); + + expect(getChipButton().textContent).toContain("Option - 1"); + + component.writeValue(null as any); + fixture.detectChanges(); + expect(getChipButton().textContent).toContain("Select an option"); + }); + }); +}); /* end of ChipSelectComponentWithDynamicOptions tests */ +@Component({ + selector: "test-app-with-dynamic-options", + template: ` + + `, + imports: [ChipSelectComponent], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +class TestAppWithDynamicOptionsComponent { + readonly firstCounter = signal(1); + readonly secondCounter = signal(2); + readonly options = computed(() => { + const first = this.firstCounter(); + const second = this.secondCounter(); + return [ + { label: `Option - ${first}`, value: "opt1", icon: "bwi-folder" }, + { label: `Option - ${second}`, value: "opt2" }, + { + label: "Parent Option", + value: "parent", + children: [ + { label: `Child - ${first}`, value: "child1" }, + { label: `Child - ${second}`, value: "child2" }, + ], + }, + ]; + }); + + readonly disabled = signal(false); + readonly fullWidth = signal(false); +} diff --git a/libs/components/src/chip-select/chip-select.component.ts b/libs/components/src/chip-select/chip-select.component.ts index 50e462dc815..1e988960472 100644 --- a/libs/components/src/chip-select/chip-select.component.ts +++ b/libs/components/src/chip-select/chip-select.component.ts @@ -106,8 +106,19 @@ export class ChipSelectComponent implements ControlValueAccessor { constructor() { // Initialize the root tree whenever options change effect(() => { + const currentSelection = this.selectedOption; + + // when the options change, clear the childParentMap + this.childParentMap.clear(); + this.initializeRootTree(this.options()); + // when the options change, we need to change our selectedOption + // to reflect the changed options. + if (currentSelection?.value != null) { + this.selectedOption = this.findOption(this.rootTree, currentSelection.value); + } + // If there's a pending value, apply it now that options are available if (this.pendingValue !== undefined) { this.selectedOption = this.findOption(this.rootTree, this.pendingValue); From 46a2af38a0db5b72f9b5125788d7588d2ca74f59 Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Thu, 19 Feb 2026 09:29:54 -0800 Subject: [PATCH 31/89] [PM-31974] - Vault Welcome dialog (#18960) * premium upgrade prompt and onboarding dialog * finalize onboard vault dialog * vault welcome dialog no ext * finish welcome dialog prompt * revert changes to unified upgrade prompt service * rename component * rename feature flag * add welcome dialog service * fix tests * fix footer position in welcome dialog * present dialog in order * fix tests * fix padding --- apps/browser/src/_locales/en/messages.json | 3 + .../vault-welcome-dialog.component.html | 27 ++++ .../vault-welcome-dialog.component.spec.ts | 87 +++++++++++++ .../vault-welcome-dialog.component.ts | 69 ++++++++++ .../services/web-vault-prompt.service.spec.ts | 21 ++- .../services/web-vault-prompt.service.ts | 11 +- .../services/welcome-dialog.service.spec.ts | 123 ++++++++++++++++++ .../vault/services/welcome-dialog.service.ts | 72 ++++++++++ .../web/src/images/welcome-dialog-graphic.png | Bin 0 -> 95551 bytes apps/web/src/locales/en/messages.json | 12 ++ libs/common/src/enums/feature-flag.enum.ts | 2 + libs/state/src/core/state-definitions.ts | 3 + 12 files changed, 425 insertions(+), 5 deletions(-) create mode 100644 apps/web/src/app/vault/components/vault-welcome-dialog/vault-welcome-dialog.component.html create mode 100644 apps/web/src/app/vault/components/vault-welcome-dialog/vault-welcome-dialog.component.spec.ts create mode 100644 apps/web/src/app/vault/components/vault-welcome-dialog/vault-welcome-dialog.component.ts create mode 100644 apps/web/src/app/vault/services/welcome-dialog.service.spec.ts create mode 100644 apps/web/src/app/vault/services/welcome-dialog.service.ts create mode 100644 apps/web/src/images/welcome-dialog-graphic.png diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index fbfaa17a87d..51ca51960d7 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -2860,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration of a list of logins that are at-risk." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", "description": "Description of the generate password slide on the at-risk password page carousel" diff --git a/apps/web/src/app/vault/components/vault-welcome-dialog/vault-welcome-dialog.component.html b/apps/web/src/app/vault/components/vault-welcome-dialog/vault-welcome-dialog.component.html new file mode 100644 index 00000000000..3304fa3e3cc --- /dev/null +++ b/apps/web/src/app/vault/components/vault-welcome-dialog/vault-welcome-dialog.component.html @@ -0,0 +1,27 @@ + +
+ +
+
+

+ {{ "vaultWelcomeDialogTitle" | i18n }} +

+

+ {{ "vaultWelcomeDialogDescription" | i18n }} +

+
+
+
+
+ + +
+
diff --git a/apps/web/src/app/vault/components/vault-welcome-dialog/vault-welcome-dialog.component.spec.ts b/apps/web/src/app/vault/components/vault-welcome-dialog/vault-welcome-dialog.component.spec.ts new file mode 100644 index 00000000000..bc0142b374d --- /dev/null +++ b/apps/web/src/app/vault/components/vault-welcome-dialog/vault-welcome-dialog.component.spec.ts @@ -0,0 +1,87 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { BehaviorSubject } from "rxjs"; + +import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { UserId } from "@bitwarden/common/types/guid"; +import { DialogRef } from "@bitwarden/components"; +import { StateProvider } from "@bitwarden/state"; + +import { + VaultWelcomeDialogComponent, + VaultWelcomeDialogResult, +} from "./vault-welcome-dialog.component"; + +describe("VaultWelcomeDialogComponent", () => { + let component: VaultWelcomeDialogComponent; + let fixture: ComponentFixture; + + const mockUserId = "user-123" as UserId; + const activeAccount$ = new BehaviorSubject({ + id: mockUserId, + } as Account); + const setUserState = jest.fn().mockResolvedValue([mockUserId, true]); + const close = jest.fn(); + + beforeEach(async () => { + jest.clearAllMocks(); + + await TestBed.configureTestingModule({ + imports: [VaultWelcomeDialogComponent], + providers: [ + { provide: AccountService, useValue: { activeAccount$ } }, + { provide: StateProvider, useValue: { setUserState } }, + { provide: DialogRef, useValue: { close } }, + { provide: I18nService, useValue: { t: (key: string) => key } }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(VaultWelcomeDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + describe("onDismiss", () => { + it("should set acknowledged state and close with Dismissed result", async () => { + await component["onDismiss"](); + + expect(setUserState).toHaveBeenCalledWith( + expect.objectContaining({ key: "vaultWelcomeDialogAcknowledged" }), + true, + mockUserId, + ); + expect(close).toHaveBeenCalledWith(VaultWelcomeDialogResult.Dismissed); + }); + + it("should throw if no active account", async () => { + activeAccount$.next(null); + + await expect(component["onDismiss"]()).rejects.toThrow("Null or undefined account"); + + expect(setUserState).not.toHaveBeenCalled(); + }); + }); + + describe("onPrimaryCta", () => { + it("should set acknowledged state and close with GetStarted result", async () => { + activeAccount$.next({ id: mockUserId } as Account); + + await component["onPrimaryCta"](); + + expect(setUserState).toHaveBeenCalledWith( + expect.objectContaining({ key: "vaultWelcomeDialogAcknowledged" }), + true, + mockUserId, + ); + expect(close).toHaveBeenCalledWith(VaultWelcomeDialogResult.GetStarted); + }); + + it("should throw if no active account", async () => { + activeAccount$.next(null); + + await expect(component["onPrimaryCta"]()).rejects.toThrow("Null or undefined account"); + + expect(setUserState).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/apps/web/src/app/vault/components/vault-welcome-dialog/vault-welcome-dialog.component.ts b/apps/web/src/app/vault/components/vault-welcome-dialog/vault-welcome-dialog.component.ts new file mode 100644 index 00000000000..d43ea5165f7 --- /dev/null +++ b/apps/web/src/app/vault/components/vault-welcome-dialog/vault-welcome-dialog.component.ts @@ -0,0 +1,69 @@ +import { CommonModule } from "@angular/common"; +import { ChangeDetectionStrategy, Component, inject } from "@angular/core"; +import { firstValueFrom } from "rxjs"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; +import { + ButtonModule, + DialogModule, + DialogRef, + DialogService, + TypographyModule, + CenterPositionStrategy, +} from "@bitwarden/components"; +import { StateProvider, UserKeyDefinition, VAULT_WELCOME_DIALOG_DISK } from "@bitwarden/state"; + +export const VaultWelcomeDialogResult = { + Dismissed: "dismissed", + GetStarted: "getStarted", +} as const; + +export type VaultWelcomeDialogResult = + (typeof VaultWelcomeDialogResult)[keyof typeof VaultWelcomeDialogResult]; + +const VAULT_WELCOME_DIALOG_ACKNOWLEDGED_KEY = new UserKeyDefinition( + VAULT_WELCOME_DIALOG_DISK, + "vaultWelcomeDialogAcknowledged", + { + deserializer: (value) => value, + clearOn: [], + }, +); + +@Component({ + selector: "app-vault-welcome-dialog", + templateUrl: "./vault-welcome-dialog.component.html", + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [CommonModule, DialogModule, ButtonModule, TypographyModule, JslibModule], +}) +export class VaultWelcomeDialogComponent { + private accountService = inject(AccountService); + private stateProvider = inject(StateProvider); + + constructor(private dialogRef: DialogRef) {} + + protected async onDismiss(): Promise { + await this.setAcknowledged(); + this.dialogRef.close(VaultWelcomeDialogResult.Dismissed); + } + + protected async onPrimaryCta(): Promise { + await this.setAcknowledged(); + this.dialogRef.close(VaultWelcomeDialogResult.GetStarted); + } + + private async setAcknowledged(): Promise { + const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + await this.stateProvider.setUserState(VAULT_WELCOME_DIALOG_ACKNOWLEDGED_KEY, true, userId); + } + + static open(dialogService: DialogService): DialogRef { + return dialogService.open(VaultWelcomeDialogComponent, { + disableClose: true, + positionStrategy: new CenterPositionStrategy(), + }); + } +} diff --git a/apps/web/src/app/vault/services/web-vault-prompt.service.spec.ts b/apps/web/src/app/vault/services/web-vault-prompt.service.spec.ts index a224b8e7c4b..eb72c80fe04 100644 --- a/apps/web/src/app/vault/services/web-vault-prompt.service.spec.ts +++ b/apps/web/src/app/vault/services/web-vault-prompt.service.spec.ts @@ -7,7 +7,7 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { UserId } from "@bitwarden/common/types/guid"; import { DialogRef, DialogService } from "@bitwarden/components"; @@ -21,6 +21,7 @@ import { import { UnifiedUpgradePromptService } from "../../billing/individual/upgrade/services"; import { WebVaultPromptService } from "./web-vault-prompt.service"; +import { WelcomeDialogService } from "./welcome-dialog.service"; describe("WebVaultPromptService", () => { let service: WebVaultPromptService; @@ -38,20 +39,33 @@ describe("WebVaultPromptService", () => { ); const upsertAutoConfirm = jest.fn().mockResolvedValue(undefined); const organizations$ = jest.fn().mockReturnValue(of([])); - const displayUpgradePromptConditionally = jest.fn().mockResolvedValue(undefined); + const displayUpgradePromptConditionally = jest.fn().mockResolvedValue(false); const enforceOrganizationDataOwnership = jest.fn().mockResolvedValue(undefined); + const conditionallyShowWelcomeDialog = jest.fn().mockResolvedValue(false); const logError = jest.fn(); + let activeAccount$: BehaviorSubject; + + function createAccount(overrides: Partial = {}): Account { + return { + id: mockUserId, + creationDate: new Date(), + ...overrides, + } as Account; + } + beforeEach(() => { jest.clearAllMocks(); + activeAccount$ = new BehaviorSubject(createAccount()); + TestBed.configureTestingModule({ providers: [ WebVaultPromptService, { provide: UnifiedUpgradePromptService, useValue: { displayUpgradePromptConditionally } }, { provide: VaultItemsTransferService, useValue: { enforceOrganizationDataOwnership } }, { provide: PolicyService, useValue: { policies$ } }, - { provide: AccountService, useValue: { activeAccount$: of({ id: mockUserId }) } }, + { provide: AccountService, useValue: { activeAccount$ } }, { provide: AutomaticUserConfirmationService, useValue: { configuration$: configurationAutoConfirm$, upsert: upsertAutoConfirm }, @@ -60,6 +74,7 @@ describe("WebVaultPromptService", () => { { provide: ConfigService, useValue: { getFeatureFlag$ } }, { provide: DialogService, useValue: { open } }, { provide: LogService, useValue: { error: logError } }, + { provide: WelcomeDialogService, useValue: { conditionallyShowWelcomeDialog } }, ], }); diff --git a/apps/web/src/app/vault/services/web-vault-prompt.service.ts b/apps/web/src/app/vault/services/web-vault-prompt.service.ts index 1774bfcc085..4c4e7a3fe78 100644 --- a/apps/web/src/app/vault/services/web-vault-prompt.service.ts +++ b/apps/web/src/app/vault/services/web-vault-prompt.service.ts @@ -20,6 +20,8 @@ import { } from "../../admin-console/organizations/policies"; import { UnifiedUpgradePromptService } from "../../billing/individual/upgrade/services"; +import { WelcomeDialogService } from "./welcome-dialog.service"; + @Injectable() export class WebVaultPromptService { private unifiedUpgradePromptService = inject(UnifiedUpgradePromptService); @@ -31,6 +33,7 @@ export class WebVaultPromptService { private configService = inject(ConfigService); private dialogService = inject(DialogService); private logService = inject(LogService); + private welcomeDialogService = inject(WelcomeDialogService); private userId$ = this.accountService.activeAccount$.pipe(getUserId); @@ -46,9 +49,13 @@ export class WebVaultPromptService { async conditionallyPromptUser() { const userId = await firstValueFrom(this.userId$); - void this.unifiedUpgradePromptService.displayUpgradePromptConditionally(); + if (await this.unifiedUpgradePromptService.displayUpgradePromptConditionally()) { + return; + } - void this.vaultItemTransferService.enforceOrganizationDataOwnership(userId); + await this.vaultItemTransferService.enforceOrganizationDataOwnership(userId); + + await this.welcomeDialogService.conditionallyShowWelcomeDialog(); this.checkForAutoConfirm(); } diff --git a/apps/web/src/app/vault/services/welcome-dialog.service.spec.ts b/apps/web/src/app/vault/services/welcome-dialog.service.spec.ts new file mode 100644 index 00000000000..752514ca066 --- /dev/null +++ b/apps/web/src/app/vault/services/welcome-dialog.service.spec.ts @@ -0,0 +1,123 @@ +import { TestBed } from "@angular/core/testing"; +import { BehaviorSubject, of } from "rxjs"; + +import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { UserId } from "@bitwarden/common/types/guid"; +import { DialogRef, DialogService } from "@bitwarden/components"; +import { StateProvider } from "@bitwarden/state"; + +import { VaultWelcomeDialogComponent } from "../components/vault-welcome-dialog/vault-welcome-dialog.component"; + +import { WelcomeDialogService } from "./welcome-dialog.service"; + +describe("WelcomeDialogService", () => { + let service: WelcomeDialogService; + + const mockUserId = "user-123" as UserId; + + const getFeatureFlag = jest.fn().mockResolvedValue(false); + const getUserState$ = jest.fn().mockReturnValue(of(false)); + const mockDialogOpen = jest.spyOn(VaultWelcomeDialogComponent, "open"); + + let activeAccount$: BehaviorSubject; + + function createAccount(overrides: Partial = {}): Account { + return { + id: mockUserId, + creationDate: new Date(), + ...overrides, + } as Account; + } + + beforeEach(() => { + jest.clearAllMocks(); + mockDialogOpen.mockReset(); + + activeAccount$ = new BehaviorSubject(createAccount()); + + TestBed.configureTestingModule({ + providers: [ + WelcomeDialogService, + { provide: AccountService, useValue: { activeAccount$ } }, + { provide: ConfigService, useValue: { getFeatureFlag } }, + { provide: DialogService, useValue: {} }, + { provide: StateProvider, useValue: { getUserState$ } }, + ], + }); + + service = TestBed.inject(WelcomeDialogService); + }); + + describe("conditionallyShowWelcomeDialog", () => { + it("should not show dialog when no active account", async () => { + activeAccount$.next(null); + + await service.conditionallyShowWelcomeDialog(); + + expect(mockDialogOpen).not.toHaveBeenCalled(); + }); + + it("should not show dialog when feature flag is disabled", async () => { + getFeatureFlag.mockResolvedValueOnce(false); + + await service.conditionallyShowWelcomeDialog(); + + expect(getFeatureFlag).toHaveBeenCalledWith(FeatureFlag.PM29437_WelcomeDialog); + expect(mockDialogOpen).not.toHaveBeenCalled(); + }); + + it("should not show dialog when account has no creation date", async () => { + activeAccount$.next(createAccount({ creationDate: undefined })); + getFeatureFlag.mockResolvedValueOnce(true); + + await service.conditionallyShowWelcomeDialog(); + + expect(mockDialogOpen).not.toHaveBeenCalled(); + }); + + it("should not show dialog when account is older than 30 days", async () => { + const overThirtyDaysAgo = new Date(Date.now() - 1000 * 60 * 60 * 24 * 30 - 1000); + activeAccount$.next(createAccount({ creationDate: overThirtyDaysAgo })); + getFeatureFlag.mockResolvedValueOnce(true); + + await service.conditionallyShowWelcomeDialog(); + + expect(mockDialogOpen).not.toHaveBeenCalled(); + }); + + it("should not show dialog when user has already acknowledged it", async () => { + activeAccount$.next(createAccount({ creationDate: new Date() })); + getFeatureFlag.mockResolvedValueOnce(true); + getUserState$.mockReturnValueOnce(of(true)); + + await service.conditionallyShowWelcomeDialog(); + + expect(mockDialogOpen).not.toHaveBeenCalled(); + }); + + it("should show dialog for new user who has not acknowledged", async () => { + activeAccount$.next(createAccount({ creationDate: new Date() })); + getFeatureFlag.mockResolvedValueOnce(true); + getUserState$.mockReturnValueOnce(of(false)); + mockDialogOpen.mockReturnValue({ closed: of(undefined) } as DialogRef); + + await service.conditionallyShowWelcomeDialog(); + + expect(mockDialogOpen).toHaveBeenCalled(); + }); + + it("should show dialog for account created exactly 30 days ago", async () => { + const exactlyThirtyDaysAgo = new Date(Date.now() - 1000 * 60 * 60 * 24 * 30); + activeAccount$.next(createAccount({ creationDate: exactlyThirtyDaysAgo })); + getFeatureFlag.mockResolvedValueOnce(true); + getUserState$.mockReturnValueOnce(of(false)); + mockDialogOpen.mockReturnValue({ closed: of(undefined) } as DialogRef); + + await service.conditionallyShowWelcomeDialog(); + + expect(mockDialogOpen).toHaveBeenCalled(); + }); + }); +}); diff --git a/apps/web/src/app/vault/services/welcome-dialog.service.ts b/apps/web/src/app/vault/services/welcome-dialog.service.ts new file mode 100644 index 00000000000..25b24b6df2d --- /dev/null +++ b/apps/web/src/app/vault/services/welcome-dialog.service.ts @@ -0,0 +1,72 @@ +import { inject, Injectable } from "@angular/core"; +import { firstValueFrom, map } from "rxjs"; + +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { DialogService } from "@bitwarden/components"; +import { StateProvider, UserKeyDefinition, VAULT_WELCOME_DIALOG_DISK } from "@bitwarden/state"; + +import { VaultWelcomeDialogComponent } from "../components/vault-welcome-dialog/vault-welcome-dialog.component"; + +const VAULT_WELCOME_DIALOG_ACKNOWLEDGED_KEY = new UserKeyDefinition( + VAULT_WELCOME_DIALOG_DISK, + "vaultWelcomeDialogAcknowledged", + { + deserializer: (value) => value, + clearOn: [], + }, +); + +const THIRTY_DAY_MS = 1000 * 60 * 60 * 24 * 30; + +@Injectable({ providedIn: "root" }) +export class WelcomeDialogService { + private accountService = inject(AccountService); + private configService = inject(ConfigService); + private dialogService = inject(DialogService); + private stateProvider = inject(StateProvider); + + /** + * Conditionally shows the welcome dialog to new users. + * + * @returns true if the dialog was shown, false otherwise + */ + async conditionallyShowWelcomeDialog() { + const account = await firstValueFrom(this.accountService.activeAccount$); + if (!account) { + return; + } + + const enabled = await this.configService.getFeatureFlag(FeatureFlag.PM29437_WelcomeDialog); + if (!enabled) { + return; + } + + const createdAt = account.creationDate; + if (!createdAt) { + return; + } + + const ageMs = Date.now() - createdAt.getTime(); + const isNewUser = ageMs >= 0 && ageMs <= THIRTY_DAY_MS; + if (!isNewUser) { + return; + } + + const acknowledged = await firstValueFrom( + this.stateProvider + .getUserState$(VAULT_WELCOME_DIALOG_ACKNOWLEDGED_KEY, account.id) + .pipe(map((v) => v ?? false)), + ); + + if (acknowledged) { + return; + } + + const dialogRef = VaultWelcomeDialogComponent.open(this.dialogService); + await firstValueFrom(dialogRef.closed); + + return; + } +} diff --git a/apps/web/src/images/welcome-dialog-graphic.png b/apps/web/src/images/welcome-dialog-graphic.png new file mode 100644 index 0000000000000000000000000000000000000000..fd2a12c52727b3b98438173de4a6d813bec1ac02 GIT binary patch literal 95551 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 B{Bi&Q literal 0 HcmV?d00001 diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index c45d7e5d630..b7ac6da15bc 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -12920,6 +12920,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index b7fad43ebbf..7b1013077d7 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -69,6 +69,7 @@ export enum FeatureFlag { BrowserPremiumSpotlight = "pm-23384-browser-premium-spotlight", MigrateMyVaultToMyItems = "pm-20558-migrate-myvault-to-myitems", PM27632_SdkCipherCrudOperations = "pm-27632-cipher-crud-operations-to-sdk", + PM29437_WelcomeDialog = "pm-29437-welcome-dialog-no-ext-prompt", PM31039ItemActionInExtension = "pm-31039-item-action-in-extension", /* Platform */ @@ -135,6 +136,7 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.BrowserPremiumSpotlight]: FALSE, [FeatureFlag.PM27632_SdkCipherCrudOperations]: FALSE, [FeatureFlag.MigrateMyVaultToMyItems]: FALSE, + [FeatureFlag.PM29437_WelcomeDialog]: FALSE, /* Auth */ [FeatureFlag.PM23801_PrefetchPasswordPrelogin]: FALSE, diff --git a/libs/state/src/core/state-definitions.ts b/libs/state/src/core/state-definitions.ts index ae6938b2069..30ee2be0592 100644 --- a/libs/state/src/core/state-definitions.ts +++ b/libs/state/src/core/state-definitions.ts @@ -212,6 +212,9 @@ export const SETUP_EXTENSION_DISMISSED_DISK = new StateDefinition( web: "disk-local", }, ); +export const VAULT_WELCOME_DIALOG_DISK = new StateDefinition("vaultWelcomeDialog", "disk", { + web: "disk-local", +}); export const VAULT_BROWSER_INTRO_CAROUSEL = new StateDefinition( "vaultBrowserIntroCarousel", "disk", From 04aad4432206ce4f19edf2cd28aec71a0f2b4837 Mon Sep 17 00:00:00 2001 From: Mike Amirault Date: Thu, 19 Feb 2026 12:54:15 -0500 Subject: [PATCH 32/89] [PM-31774] Remove toggle visibility callout on hidden text sends (#18924) --- .../send-access-text.component.html | 24 ++++++------------- .../send-access/send-access-text.component.ts | 4 ++-- apps/web/src/locales/en/messages.json | 4 ---- 3 files changed, 9 insertions(+), 23 deletions(-) diff --git a/apps/web/src/app/tools/send/send-access/send-access-text.component.html b/apps/web/src/app/tools/send/send-access/send-access-text.component.html index ca772251146..c7fa148169d 100644 --- a/apps/web/src/app/tools/send/send-access/send-access-text.component.html +++ b/apps/web/src/app/tools/send/send-access/send-access-text.component.html @@ -1,26 +1,16 @@ -{{ "sendHiddenByDefault" | i18n }}
- + @if (send.text.hidden) { + + }
diff --git a/apps/web/src/app/tools/send/send-access/send-access-text.component.ts b/apps/web/src/app/tools/send/send-access/send-access-text.component.ts index 794cfbc9678..8a947eafb69 100644 --- a/apps/web/src/app/tools/send/send-access/send-access-text.component.ts +++ b/apps/web/src/app/tools/send/send-access/send-access-text.component.ts @@ -6,7 +6,7 @@ import { FormBuilder } from "@angular/forms"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { SendAccessView } from "@bitwarden/common/tools/send/models/view/send-access.view"; -import { ToastService } from "@bitwarden/components"; +import { IconModule, ToastService } from "@bitwarden/components"; import { SharedModule } from "../../../shared"; @@ -15,7 +15,7 @@ import { SharedModule } from "../../../shared"; @Component({ selector: "app-send-access-text", templateUrl: "send-access-text.component.html", - imports: [SharedModule], + imports: [SharedModule, IconModule], }) export class SendAccessTextComponent { private _send: SendAccessView = null; diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index b7ac6da15bc..2d9cba6d409 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -5855,10 +5855,6 @@ "message": "Don't know the password? Ask the sender for the password needed to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "This Send is hidden by default. You can toggle its visibility using the button below.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Download attachments" }, From 8399815ea7bfe00a251a0091ee8a504c5a1bf784 Mon Sep 17 00:00:00 2001 From: John Harrington <84741727+harr1424@users.noreply.github.com> Date: Thu, 19 Feb 2026 11:59:59 -0700 Subject: [PATCH 33/89] [PM-32237] Add back functionality to email OTP auth flow (#19024) * add back functionality to OTP auth flow * respond to review comments * hoist email value to parent component --------- Co-authored-by: Alex Dragovich <46065570+itsadrago@users.noreply.github.com> --- .../send-access-email.component.html | 26 ++++++------ .../send-access-email.component.ts | 41 +++++++++++++++++-- .../send/send-access/send-auth.component.html | 1 + .../send/send-access/send-auth.component.ts | 7 ++++ 4 files changed, 60 insertions(+), 15 deletions(-) diff --git a/apps/web/src/app/tools/send/send-access/send-access-email.component.html b/apps/web/src/app/tools/send/send-access/send-access-email.component.html index ee5a03670bb..82ef9a397c5 100644 --- a/apps/web/src/app/tools/send/send-access/send-access-email.component.html +++ b/apps/web/src/app/tools/send/send-access/send-access-email.component.html @@ -20,16 +20,18 @@ {{ "verificationCode" | i18n }} -
- -
+ + } diff --git a/apps/web/src/app/tools/send/send-access/send-access-email.component.ts b/apps/web/src/app/tools/send/send-access/send-access-email.component.ts index b1374cd6c66..0915a47e4ad 100644 --- a/apps/web/src/app/tools/send/send-access/send-access-email.component.ts +++ b/apps/web/src/app/tools/send/send-access/send-access-email.component.ts @@ -1,6 +1,14 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { ChangeDetectionStrategy, Component, input, OnDestroy, OnInit } from "@angular/core"; +import { + ChangeDetectionStrategy, + Component, + effect, + input, + OnDestroy, + OnInit, + output, +} from "@angular/core"; import { FormControl, FormGroup, Validators } from "@angular/forms"; import { SharedModule } from "../../../shared"; @@ -18,18 +26,45 @@ export class SendAccessEmailComponent implements OnInit, OnDestroy { protected otp: FormControl; readonly loading = input.required(); + readonly backToEmail = output(); constructor() {} ngOnInit() { this.email = new FormControl("", Validators.required); - this.otp = new FormControl("", Validators.required); + this.otp = new FormControl(""); this.formGroup().addControl("email", this.email); this.formGroup().addControl("otp", this.otp); - } + // Update validators when enterOtp changes + effect(() => { + const isOtpMode = this.enterOtp(); + if (isOtpMode) { + // In OTP mode: email is not required (already entered), otp is required + this.email.clearValidators(); + this.otp.setValidators([Validators.required]); + } else { + // In email mode: email is required, otp is not required + this.email.setValidators([Validators.required]); + this.otp.clearValidators(); + } + this.email.updateValueAndValidity(); + this.otp.updateValueAndValidity(); + }); + } ngOnDestroy() { this.formGroup().removeControl("email"); this.formGroup().removeControl("otp"); } + + onBackClick() { + this.backToEmail.emit(); + if (this.otp) { + this.otp.clearValidators(); + this.otp.setValue(""); + this.otp.setErrors(null); + this.otp.markAsUntouched(); + this.otp.markAsPristine(); + } + } } diff --git a/apps/web/src/app/tools/send/send-access/send-auth.component.html b/apps/web/src/app/tools/send/send-access/send-auth.component.html index c3e90cea4ea..fa5bef77274 100644 --- a/apps/web/src/app/tools/send/send-access/send-auth.component.html +++ b/apps/web/src/app/tools/send/send-access/send-auth.component.html @@ -31,6 +31,7 @@ [formGroup]="sendAccessForm" [enterOtp]="enterOtp()" [loading]="loading()" + (backToEmail)="onBackToEmail()" > } } diff --git a/apps/web/src/app/tools/send/send-access/send-auth.component.ts b/apps/web/src/app/tools/send/send-access/send-auth.component.ts index 994bd7f3ee3..97b71778539 100644 --- a/apps/web/src/app/tools/send/send-access/send-auth.component.ts +++ b/apps/web/src/app/tools/send/send-access/send-auth.component.ts @@ -90,6 +90,11 @@ export class SendAuthComponent implements OnInit { this.loading.set(false); } + onBackToEmail() { + this.enterOtp.set(false); + this.updatePageTitle(); + } + private async attemptV1Access() { try { const accessRequest = new SendAccessRequest(); @@ -247,10 +252,12 @@ export class SendAuthComponent implements OnInit { if (this.enterOtp()) { this.anonLayoutWrapperDataService.setAnonLayoutWrapperData({ pageTitle: { key: "enterTheCodeSentToYourEmail" }, + pageSubtitle: this.sendAccessForm.value.email ?? null, }); } else { this.anonLayoutWrapperDataService.setAnonLayoutWrapperData({ pageTitle: { key: "verifyYourEmailToViewThisSend" }, + pageSubtitle: null, }); } } else if (authType === AuthType.Password) { From caa28ac5b3a4d339a967a6dc1d360445e80128ec Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Thu, 19 Feb 2026 21:18:17 +0100 Subject: [PATCH 34/89] [PM-32481] Apply same custom scrollbar to nav (#19083) * Apply same custom scrollbar to nav * Split colors --- apps/desktop/src/scss/base.scss | 25 ++++++++++++++++++++++--- apps/desktop/src/scss/variables.scss | 4 ++++ 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/apps/desktop/src/scss/base.scss b/apps/desktop/src/scss/base.scss index a95d82dacd4..2371192e0ea 100644 --- a/apps/desktop/src/scss/base.scss +++ b/apps/desktop/src/scss/base.scss @@ -102,23 +102,30 @@ textarea { div:not(.modal)::-webkit-scrollbar, .cdk-virtual-scroll-viewport::-webkit-scrollbar, -.vault-filters::-webkit-scrollbar { +.vault-filters::-webkit-scrollbar, +#bit-side-nav::-webkit-scrollbar { width: 10px; height: 10px; } div:not(.modal)::-webkit-scrollbar-track, .cdk-virtual-scroll-viewport::-webkit-scrollbar-track, -.vault-filters::-webkit-scrollbar-track { +.vault-filters::-webkit-scrollbar-track, +#bit-side-nav::-webkit-scrollbar-track { background-color: transparent; } div:not(.modal)::-webkit-scrollbar-thumb, .cdk-virtual-scroll-viewport::-webkit-scrollbar-thumb, -.vault-filters::-webkit-scrollbar-thumb { +.vault-filters::-webkit-scrollbar-thumb, +#bit-side-nav::-webkit-scrollbar-thumb { border-radius: 10px; margin-right: 1px; +} +div:not(.modal)::-webkit-scrollbar-thumb, +.cdk-virtual-scroll-viewport::-webkit-scrollbar-thumb, +.vault-filters::-webkit-scrollbar-thumb { @include themify($themes) { background-color: themed("scrollbarColor"); } @@ -130,6 +137,18 @@ div:not(.modal)::-webkit-scrollbar-thumb, } } +#bit-side-nav::-webkit-scrollbar-thumb { + @include themify($themes) { + background-color: themed("scrollbarColorNav"); + } + + &:hover { + @include themify($themes) { + background-color: themed("scrollbarHoverColorNav"); + } + } +} + // cdk-virtual-scroll .cdk-virtual-scroll-viewport { width: 100%; diff --git a/apps/desktop/src/scss/variables.scss b/apps/desktop/src/scss/variables.scss index a00257ed608..51a6d2ac840 100644 --- a/apps/desktop/src/scss/variables.scss +++ b/apps/desktop/src/scss/variables.scss @@ -56,6 +56,8 @@ $themes: ( backgroundColorAlt2: $background-color-alt2, scrollbarColor: rgba(100, 100, 100, 0.2), scrollbarHoverColor: rgba(100, 100, 100, 0.4), + scrollbarColorNav: rgba(226, 226, 226), + scrollbarHoverColorNav: rgba(197, 197, 197), boxBackgroundColor: $box-background-color, boxBackgroundHoverColor: $box-background-hover-color, boxBorderColor: $box-border-color, @@ -115,6 +117,8 @@ $themes: ( backgroundColorAlt2: #15181e, scrollbarColor: #6e788a, scrollbarHoverColor: #8d94a5, + scrollbarColorNav: #6e788a, + scrollbarHoverColorNav: #8d94a5, boxBackgroundColor: #2f343d, boxBackgroundHoverColor: #3c424e, boxBorderColor: #4c525f, From 8ec9c55b1812e6843d61d1e7e339ea058de1a1d0 Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Thu, 19 Feb 2026 21:18:48 +0100 Subject: [PATCH 35/89] Adjust desktop header color (#19082) --- apps/desktop/src/scss/variables.scss | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/desktop/src/scss/variables.scss b/apps/desktop/src/scss/variables.scss index 51a6d2ac840..62d4f23ad46 100644 --- a/apps/desktop/src/scss/variables.scss +++ b/apps/desktop/src/scss/variables.scss @@ -61,10 +61,10 @@ $themes: ( boxBackgroundColor: $box-background-color, boxBackgroundHoverColor: $box-background-hover-color, boxBorderColor: $box-border-color, - headerBackgroundColor: rgb(var(--color-background-alt3)), - headerBorderColor: rgb(var(--color-background-alt4)), - headerInputBackgroundColor: darken($brand-primary, 8%), - headerInputBackgroundFocusColor: darken($brand-primary, 10%), + headerBackgroundColor: var(--color-sidenav-background), + headerBorderColor: var(--color-sidenav-active-item), + headerInputBackgroundColor: darken($brand-primary, 20%), + headerInputBackgroundFocusColor: darken($brand-primary, 25%), headerInputColor: #ffffff, headerInputPlaceholderColor: lighten($brand-primary, 35%), listItemBackgroundColor: $background-color, @@ -122,8 +122,8 @@ $themes: ( boxBackgroundColor: #2f343d, boxBackgroundHoverColor: #3c424e, boxBorderColor: #4c525f, - headerBackgroundColor: rgb(var(--color-background-alt3)), - headerBorderColor: rgb(var(--color-background-alt4)), + headerBackgroundColor: var(--color-sidenav-background), + headerBorderColor: var(--color-sidenav-active-item), headerInputBackgroundColor: #3c424e, headerInputBackgroundFocusColor: #4c525f, headerInputColor: #ffffff, From 702e6086b914509dcc3b77f10e7ccf9361ac2a2b Mon Sep 17 00:00:00 2001 From: bmbitwarden Date: Thu, 19 Feb 2026 19:26:18 -0500 Subject: [PATCH 36/89] PM-30876 resolved screenreader for icons on send table rows (#18940) * PM-30876 resolved screenreader for icons on send table rows * PM-30876 resolved grey icon issue * PM-30876 resolved blank underline issue * PM-30876 resolved screen reader * PM-30876 resolved screen reader --- .../src/send-table/send-table.component.html | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/libs/tools/send/send-ui/src/send-table/send-table.component.html b/libs/tools/send/send-ui/src/send-table/send-table.component.html index 1c235415cae..14dc93bb706 100644 --- a/libs/tools/send/send-ui/src/send-table/send-table.component.html +++ b/libs/tools/send/send-ui/src/send-table/send-table.component.html @@ -29,9 +29,9 @@ class="bwi bwi-exclamation-triangle" appStopProp title="{{ 'disabled' | i18n }}" - aria-hidden="true" + aria-label="{{ 'disabled' | i18n }}" + tabindex="0" >
- {{ "disabled" | i18n }} } @if (s.authType !== authType.None) { @let titleKey = @@ -40,36 +40,36 @@ class="bwi bwi-lock" appStopProp title="{{ titleKey | i18n }}" - aria-hidden="true" + aria-label="{{ titleKey | i18n }}" + tabindex="0" >
- {{ titleKey | i18n }} } @if (s.maxAccessCountReached) { - {{ "maxAccessCountReached" | i18n }} } @if (s.expired) { - {{ "expired" | i18n }} } @if (s.pendingDelete) { - {{ "pendingDeletion" | i18n }} }
From 36635741138bb5235083724112d653ed8850154a Mon Sep 17 00:00:00 2001 From: Alex <55413326+AlexRubik@users.noreply.github.com> Date: Thu, 19 Feb 2026 17:43:51 -0800 Subject: [PATCH 37/89] [PM-31496] Reports back button placement (#18706) * place back button fixed at bottom right * fix type errors * add the new button logic to org reports also * fix: restore keyboard focus for reports back button in CDK overlay The CDK Overlay renders outside the cdkTrapFocus boundary, making the floating "Back to reports" button unreachable via Tab. Add a focus bridge element that intercepts Tab and programmatically redirects focus to the overlay button, with a return handler to cycle focus back into the page. --- .../organization-reporting.module.ts | 9 ++- .../reporting/reports-home.component.html | 29 ++++++-- .../reporting/reports-home.component.ts | 72 ++++++++++++++++++- .../reports/reports-layout.component.html | 30 +++++--- .../dirt/reports/reports-layout.component.ts | 71 +++++++++++++++--- .../src/app/dirt/reports/reports.module.ts | 2 + 6 files changed, 187 insertions(+), 26 deletions(-) diff --git a/apps/web/src/app/admin-console/organizations/reporting/organization-reporting.module.ts b/apps/web/src/app/admin-console/organizations/reporting/organization-reporting.module.ts index 46599d7da46..d96e2cbb6c0 100644 --- a/apps/web/src/app/admin-console/organizations/reporting/organization-reporting.module.ts +++ b/apps/web/src/app/admin-console/organizations/reporting/organization-reporting.module.ts @@ -1,3 +1,4 @@ +import { OverlayModule } from "@angular/cdk/overlay"; import { NgModule } from "@angular/core"; import { ReportsSharedModule } from "../../../dirt/reports"; @@ -8,7 +9,13 @@ import { OrganizationReportingRoutingModule } from "./organization-reporting-rou import { ReportsHomeComponent } from "./reports-home.component"; @NgModule({ - imports: [SharedModule, ReportsSharedModule, OrganizationReportingRoutingModule, HeaderModule], + imports: [ + SharedModule, + OverlayModule, + ReportsSharedModule, + OrganizationReportingRoutingModule, + HeaderModule, + ], declarations: [ReportsHomeComponent], }) export class OrganizationReportingModule {} diff --git a/apps/web/src/app/admin-console/organizations/reporting/reports-home.component.html b/apps/web/src/app/admin-console/organizations/reporting/reports-home.component.html index 59eac5b6300..9a931f66af9 100644 --- a/apps/web/src/app/admin-console/organizations/reporting/reports-home.component.html +++ b/apps/web/src/app/admin-console/organizations/reporting/reports-home.component.html @@ -8,9 +8,26 @@ - +@if (!(homepage$ | async)) { + + +} + + + + diff --git a/apps/web/src/app/admin-console/organizations/reporting/reports-home.component.ts b/apps/web/src/app/admin-console/organizations/reporting/reports-home.component.ts index 6043bfd3193..503a4f88050 100644 --- a/apps/web/src/app/admin-console/organizations/reporting/reports-home.component.ts +++ b/apps/web/src/app/admin-console/organizations/reporting/reports-home.component.ts @@ -1,6 +1,18 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { Component, OnInit } from "@angular/core"; +import { Overlay, OverlayRef } from "@angular/cdk/overlay"; +import { TemplatePortal } from "@angular/cdk/portal"; +import { + AfterViewInit, + Component, + inject, + OnDestroy, + OnInit, + TemplateRef, + viewChild, + ViewContainerRef, +} from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { ActivatedRoute, NavigationEnd, Router } from "@angular/router"; import { filter, map, Observable, startWith, concatMap, firstValueFrom } from "rxjs"; @@ -21,16 +33,30 @@ import { ReportVariant, reports, ReportType, ReportEntry } from "../../../dirt/r templateUrl: "reports-home.component.html", standalone: false, }) -export class ReportsHomeComponent implements OnInit { +export class ReportsHomeComponent implements OnInit, AfterViewInit, OnDestroy { reports$: Observable; homepage$: Observable; + private readonly backButtonTemplate = + viewChild.required>("backButtonTemplate"); + + private overlayRef: OverlayRef | null = null; + private overlay = inject(Overlay); + private viewContainerRef = inject(ViewContainerRef); + constructor( private route: ActivatedRoute, private organizationService: OrganizationService, private accountService: AccountService, private router: Router, - ) {} + ) { + this.router.events + .pipe( + takeUntilDestroyed(), + filter((event) => event instanceof NavigationEnd), + ) + .subscribe(() => this.updateOverlay()); + } async ngOnInit() { this.homepage$ = this.router.events.pipe( @@ -51,6 +77,46 @@ export class ReportsHomeComponent implements OnInit { ); } + ngAfterViewInit(): void { + this.updateOverlay(); + } + + ngOnDestroy(): void { + this.overlayRef?.dispose(); + } + + returnFocusToPage(event: Event): void { + if ((event as KeyboardEvent).shiftKey) { + return; // Allow natural Shift+Tab behavior + } + event.preventDefault(); + const firstFocusable = document.querySelector( + "[cdktrapfocus] a:not([tabindex='-1'])", + ) as HTMLElement; + firstFocusable?.focus(); + } + + focusOverlayButton(event: Event): void { + if ((event as KeyboardEvent).shiftKey) { + return; // Allow natural Shift+Tab behavior + } + event.preventDefault(); + const button = this.overlayRef?.overlayElement?.querySelector("a") as HTMLElement; + button?.focus(); + } + + private updateOverlay(): void { + if (this.isReportsHomepageRouteUrl(this.router.url)) { + this.overlayRef?.dispose(); + this.overlayRef = null; + } else if (!this.overlayRef) { + this.overlayRef = this.overlay.create({ + positionStrategy: this.overlay.position().global().bottom("20px").right("32px"), + }); + this.overlayRef.attach(new TemplatePortal(this.backButtonTemplate(), this.viewContainerRef)); + } + } + private buildReports(productType: ProductTierType): ReportEntry[] { const reportRequiresUpgrade = productType == ProductTierType.Free ? ReportVariant.RequiresUpgrade : ReportVariant.Enabled; diff --git a/apps/web/src/app/dirt/reports/reports-layout.component.html b/apps/web/src/app/dirt/reports/reports-layout.component.html index 0cb5d304a34..c290fc88335 100644 --- a/apps/web/src/app/dirt/reports/reports-layout.component.html +++ b/apps/web/src/app/dirt/reports/reports-layout.component.html @@ -1,11 +1,25 @@ -
-
- @if (!homepage) { - - {{ "backToReports" | i18n }} - - } +@if (!homepage) { + + +} + + + -
+ diff --git a/apps/web/src/app/dirt/reports/reports-layout.component.ts b/apps/web/src/app/dirt/reports/reports-layout.component.ts index a6d84ccb037..136b70c81e4 100644 --- a/apps/web/src/app/dirt/reports/reports-layout.component.ts +++ b/apps/web/src/app/dirt/reports/reports-layout.component.ts @@ -1,4 +1,14 @@ -import { Component } from "@angular/core"; +import { Overlay, OverlayRef } from "@angular/cdk/overlay"; +import { TemplatePortal } from "@angular/cdk/portal"; +import { + AfterViewInit, + Component, + inject, + OnDestroy, + TemplateRef, + viewChild, + ViewContainerRef, +} from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { NavigationEnd, Router } from "@angular/router"; import { filter } from "rxjs/operators"; @@ -10,20 +20,65 @@ import { filter } from "rxjs/operators"; templateUrl: "reports-layout.component.html", standalone: false, }) -export class ReportsLayoutComponent { +export class ReportsLayoutComponent implements AfterViewInit, OnDestroy { homepage = true; - constructor(router: Router) { - const reportsHomeRoute = "/reports"; + private readonly backButtonTemplate = + viewChild.required>("backButtonTemplate"); - this.homepage = router.url === reportsHomeRoute; - router.events + private overlayRef: OverlayRef | null = null; + private overlay = inject(Overlay); + private viewContainerRef = inject(ViewContainerRef); + private router = inject(Router); + + constructor() { + this.router.events .pipe( takeUntilDestroyed(), filter((event) => event instanceof NavigationEnd), ) - .subscribe((event) => { - this.homepage = (event as NavigationEnd).url == reportsHomeRoute; + .subscribe(() => this.updateOverlay()); + } + + ngAfterViewInit(): void { + this.updateOverlay(); + } + + ngOnDestroy(): void { + this.overlayRef?.dispose(); + } + + returnFocusToPage(event: Event): void { + if ((event as KeyboardEvent).shiftKey) { + return; // Allow natural Shift+Tab behavior + } + event.preventDefault(); + const firstFocusable = document.querySelector( + "[cdktrapfocus] a:not([tabindex='-1'])", + ) as HTMLElement; + firstFocusable?.focus(); + } + + focusOverlayButton(event: Event): void { + if ((event as KeyboardEvent).shiftKey) { + return; // Allow natural Shift+Tab behavior + } + event.preventDefault(); + const button = this.overlayRef?.overlayElement?.querySelector("a") as HTMLElement; + button?.focus(); + } + + private updateOverlay(): void { + if (this.router.url === "/reports") { + this.homepage = true; + this.overlayRef?.dispose(); + this.overlayRef = null; + } else if (!this.overlayRef) { + this.homepage = false; + this.overlayRef = this.overlay.create({ + positionStrategy: this.overlay.position().global().bottom("20px").right("32px"), }); + this.overlayRef.attach(new TemplatePortal(this.backButtonTemplate(), this.viewContainerRef)); + } } } diff --git a/apps/web/src/app/dirt/reports/reports.module.ts b/apps/web/src/app/dirt/reports/reports.module.ts index 4fc152917f4..c4bd9fef809 100644 --- a/apps/web/src/app/dirt/reports/reports.module.ts +++ b/apps/web/src/app/dirt/reports/reports.module.ts @@ -1,3 +1,4 @@ +import { OverlayModule } from "@angular/cdk/overlay"; import { CommonModule } from "@angular/common"; import { NgModule } from "@angular/core"; @@ -29,6 +30,7 @@ import { ReportsSharedModule } from "./shared"; @NgModule({ imports: [ CommonModule, + OverlayModule, SharedModule, ReportsSharedModule, ReportsRoutingModule, From b0549dbfb6be006858452db619bbc5e0020347ff Mon Sep 17 00:00:00 2001 From: "bw-ghapp[bot]" <178206702+bw-ghapp[bot]@users.noreply.github.com> Date: Fri, 20 Feb 2026 12:04:36 +0100 Subject: [PATCH 38/89] Autosync the updated translations (#19093) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/desktop/src/locales/af/messages.json | 9 +++++++ apps/desktop/src/locales/ar/messages.json | 9 +++++++ apps/desktop/src/locales/az/messages.json | 9 +++++++ apps/desktop/src/locales/be/messages.json | 9 +++++++ apps/desktop/src/locales/bg/messages.json | 9 +++++++ apps/desktop/src/locales/bn/messages.json | 9 +++++++ apps/desktop/src/locales/bs/messages.json | 9 +++++++ apps/desktop/src/locales/ca/messages.json | 9 +++++++ apps/desktop/src/locales/cs/messages.json | 9 +++++++ apps/desktop/src/locales/cy/messages.json | 9 +++++++ apps/desktop/src/locales/da/messages.json | 9 +++++++ apps/desktop/src/locales/de/messages.json | 9 +++++++ apps/desktop/src/locales/el/messages.json | 9 +++++++ apps/desktop/src/locales/en_GB/messages.json | 9 +++++++ apps/desktop/src/locales/en_IN/messages.json | 9 +++++++ apps/desktop/src/locales/eo/messages.json | 9 +++++++ apps/desktop/src/locales/es/messages.json | 9 +++++++ apps/desktop/src/locales/et/messages.json | 9 +++++++ apps/desktop/src/locales/eu/messages.json | 9 +++++++ apps/desktop/src/locales/fa/messages.json | 9 +++++++ apps/desktop/src/locales/fi/messages.json | 9 +++++++ apps/desktop/src/locales/fil/messages.json | 9 +++++++ apps/desktop/src/locales/fr/messages.json | 9 +++++++ apps/desktop/src/locales/gl/messages.json | 9 +++++++ apps/desktop/src/locales/he/messages.json | 9 +++++++ apps/desktop/src/locales/hi/messages.json | 9 +++++++ apps/desktop/src/locales/hr/messages.json | 9 +++++++ apps/desktop/src/locales/hu/messages.json | 9 +++++++ apps/desktop/src/locales/id/messages.json | 9 +++++++ apps/desktop/src/locales/it/messages.json | 9 +++++++ apps/desktop/src/locales/ja/messages.json | 9 +++++++ apps/desktop/src/locales/ka/messages.json | 9 +++++++ apps/desktop/src/locales/km/messages.json | 9 +++++++ apps/desktop/src/locales/kn/messages.json | 9 +++++++ apps/desktop/src/locales/ko/messages.json | 9 +++++++ apps/desktop/src/locales/lt/messages.json | 9 +++++++ apps/desktop/src/locales/lv/messages.json | 13 ++++++++-- apps/desktop/src/locales/me/messages.json | 9 +++++++ apps/desktop/src/locales/ml/messages.json | 9 +++++++ apps/desktop/src/locales/mr/messages.json | 9 +++++++ apps/desktop/src/locales/my/messages.json | 9 +++++++ apps/desktop/src/locales/nb/messages.json | 9 +++++++ apps/desktop/src/locales/ne/messages.json | 9 +++++++ apps/desktop/src/locales/nl/messages.json | 9 +++++++ apps/desktop/src/locales/nn/messages.json | 9 +++++++ apps/desktop/src/locales/or/messages.json | 9 +++++++ apps/desktop/src/locales/pl/messages.json | 9 +++++++ apps/desktop/src/locales/pt_BR/messages.json | 25 +++++++++++++------- apps/desktop/src/locales/pt_PT/messages.json | 9 +++++++ apps/desktop/src/locales/ro/messages.json | 9 +++++++ apps/desktop/src/locales/ru/messages.json | 9 +++++++ apps/desktop/src/locales/si/messages.json | 9 +++++++ apps/desktop/src/locales/sk/messages.json | 9 +++++++ apps/desktop/src/locales/sl/messages.json | 9 +++++++ apps/desktop/src/locales/sr/messages.json | 9 +++++++ apps/desktop/src/locales/sv/messages.json | 9 +++++++ apps/desktop/src/locales/ta/messages.json | 9 +++++++ apps/desktop/src/locales/te/messages.json | 9 +++++++ apps/desktop/src/locales/th/messages.json | 9 +++++++ apps/desktop/src/locales/tr/messages.json | 9 +++++++ apps/desktop/src/locales/uk/messages.json | 9 +++++++ apps/desktop/src/locales/vi/messages.json | 9 +++++++ apps/desktop/src/locales/zh_CN/messages.json | 13 ++++++++-- apps/desktop/src/locales/zh_TW/messages.json | 9 +++++++ 64 files changed, 588 insertions(+), 12 deletions(-) diff --git a/apps/desktop/src/locales/af/messages.json b/apps/desktop/src/locales/af/messages.json index c0824c61d03..dfcdd36da20 100644 --- a/apps/desktop/src/locales/af/messages.json +++ b/apps/desktop/src/locales/af/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Ongeldige bevestigingskode" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Gaan Voort" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/ar/messages.json b/apps/desktop/src/locales/ar/messages.json index 3e668c327b0..1273058bfc9 100644 --- a/apps/desktop/src/locales/ar/messages.json +++ b/apps/desktop/src/locales/ar/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "رمز التحقق غير صالح" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "متابعة" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/az/messages.json b/apps/desktop/src/locales/az/messages.json index 4e5d414eb1c..ba43fecfc60 100644 --- a/apps/desktop/src/locales/az/messages.json +++ b/apps/desktop/src/locales/az/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Yararsız doğrulama kodu" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Davam" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Birdən çox e-poçtu daxil edərkən vergül istifadə edin." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/be/messages.json b/apps/desktop/src/locales/be/messages.json index f2f9d0a736d..c554352c438 100644 --- a/apps/desktop/src/locales/be/messages.json +++ b/apps/desktop/src/locales/be/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Памылковы праверачны код" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Працягнуць" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/bg/messages.json b/apps/desktop/src/locales/bg/messages.json index ea0355ad7f6..6913b3b563e 100644 --- a/apps/desktop/src/locales/bg/messages.json +++ b/apps/desktop/src/locales/bg/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Грешен код за потвърждаване" }, + "invalidEmailOrVerificationCode": { + "message": "Грешна е-поща или код за потвърждаване" + }, "continue": { "message": "Продължаване" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Можете да въведете повече е-пощи, като ги разделите със запетая." }, + "emailsRequiredChangeAccessType": { + "message": "Потвърждаването на е-пощата изисква да е наличен поне един адрес на е-поща. Ако искате да премахнете всички е-пощи, променете начина за достъп по-горе." + }, "emailPlaceholder": { "message": "потребител@bitwarden.com , потребител@acme.com" + }, + "userVerificationFailed": { + "message": "Проверката на потребителя беше неуспешна." } } diff --git a/apps/desktop/src/locales/bn/messages.json b/apps/desktop/src/locales/bn/messages.json index 6a211c93052..2919e52b0fb 100644 --- a/apps/desktop/src/locales/bn/messages.json +++ b/apps/desktop/src/locales/bn/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "অবিরত" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/bs/messages.json b/apps/desktop/src/locales/bs/messages.json index 4ca3aa8ffc2..61bb17d5171 100644 --- a/apps/desktop/src/locales/bs/messages.json +++ b/apps/desktop/src/locales/bs/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Neispravan verifikacijski kod" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Nastavi" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/ca/messages.json b/apps/desktop/src/locales/ca/messages.json index 3b8562814fd..ac59b1bd040 100644 --- a/apps/desktop/src/locales/ca/messages.json +++ b/apps/desktop/src/locales/ca/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Codi de verificació no vàlid" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Continua" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/cs/messages.json b/apps/desktop/src/locales/cs/messages.json index 75136c41831..0772645a8d4 100644 --- a/apps/desktop/src/locales/cs/messages.json +++ b/apps/desktop/src/locales/cs/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Neplatný ověřovací kód" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Pokračovat" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Zadejte více e-mailů oddělených čárkou." }, + "emailsRequiredChangeAccessType": { + "message": "Ověření e-mailu vyžaduje alespoň jednu e-mailovou adresu. Chcete-li odebrat všechny emaily, změňte výše uvedený typ přístupu." + }, "emailPlaceholder": { "message": "uživatel@bitwarden.com, uživatel@společnost.cz" + }, + "userVerificationFailed": { + "message": "Ověření uživatele se nezdařilo." } } diff --git a/apps/desktop/src/locales/cy/messages.json b/apps/desktop/src/locales/cy/messages.json index 46df0aca8c5..6f39024dd17 100644 --- a/apps/desktop/src/locales/cy/messages.json +++ b/apps/desktop/src/locales/cy/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Continue" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/da/messages.json b/apps/desktop/src/locales/da/messages.json index f6abcd51740..f7f6dd31da5 100644 --- a/apps/desktop/src/locales/da/messages.json +++ b/apps/desktop/src/locales/da/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Ugyldig bekræftelseskode" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Fortsæt" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/de/messages.json b/apps/desktop/src/locales/de/messages.json index a2c346896ac..fb045e30489 100644 --- a/apps/desktop/src/locales/de/messages.json +++ b/apps/desktop/src/locales/de/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Ungültiger Verifizierungscode" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Weiter" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Gib mehrere E-Mail-Adressen ein, indem du sie mit einem Komma trennst." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "benutzer@bitwarden.com, benutzer@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/el/messages.json b/apps/desktop/src/locales/el/messages.json index 624560f5888..fe21423ceba 100644 --- a/apps/desktop/src/locales/el/messages.json +++ b/apps/desktop/src/locales/el/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Μη έγκυρος κωδικός επαλήθευσης" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Συνέχεια" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/en_GB/messages.json b/apps/desktop/src/locales/en_GB/messages.json index aaf1e12955c..04684ffe9bd 100644 --- a/apps/desktop/src/locales/en_GB/messages.json +++ b/apps/desktop/src/locales/en_GB/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Continue" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/en_IN/messages.json b/apps/desktop/src/locales/en_IN/messages.json index 1dca7070bfc..bda8ffa8fd5 100644 --- a/apps/desktop/src/locales/en_IN/messages.json +++ b/apps/desktop/src/locales/en_IN/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Continue" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/eo/messages.json b/apps/desktop/src/locales/eo/messages.json index cefd462e99f..79e1ece499d 100644 --- a/apps/desktop/src/locales/eo/messages.json +++ b/apps/desktop/src/locales/eo/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Nevalida kontrola kodo" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Daŭrigi" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/es/messages.json b/apps/desktop/src/locales/es/messages.json index d9fb17907aa..91ec21c717f 100644 --- a/apps/desktop/src/locales/es/messages.json +++ b/apps/desktop/src/locales/es/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Código de verificación incorrecto" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Continuar" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Introduce varios correos electrónicos separándolos con una coma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/et/messages.json b/apps/desktop/src/locales/et/messages.json index ba930db8961..84f432ac410 100644 --- a/apps/desktop/src/locales/et/messages.json +++ b/apps/desktop/src/locales/et/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Vale kinnituskood" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Jätka" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/eu/messages.json b/apps/desktop/src/locales/eu/messages.json index e03da9ef685..2adb34fa9f2 100644 --- a/apps/desktop/src/locales/eu/messages.json +++ b/apps/desktop/src/locales/eu/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Egiaztatze-kodea ez da baliozkoa" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Jarraitu" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/fa/messages.json b/apps/desktop/src/locales/fa/messages.json index a443cc8c2e7..94e5a54ab4b 100644 --- a/apps/desktop/src/locales/fa/messages.json +++ b/apps/desktop/src/locales/fa/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "کد تأیید نامعتبر است" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "ادامه" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/fi/messages.json b/apps/desktop/src/locales/fi/messages.json index c7b51def9b2..78eedc7e1ce 100644 --- a/apps/desktop/src/locales/fi/messages.json +++ b/apps/desktop/src/locales/fi/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Virheellinen todennuskoodi" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Jatka" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/fil/messages.json b/apps/desktop/src/locales/fil/messages.json index 5835821f526..25dd12dd51e 100644 --- a/apps/desktop/src/locales/fil/messages.json +++ b/apps/desktop/src/locales/fil/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Maling verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Magpatuloy" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/fr/messages.json b/apps/desktop/src/locales/fr/messages.json index e8d07e28d2d..04e0725c9b4 100644 --- a/apps/desktop/src/locales/fr/messages.json +++ b/apps/desktop/src/locales/fr/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Code de vérification invalide" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Continuer" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/gl/messages.json b/apps/desktop/src/locales/gl/messages.json index 6d0922bd680..3b80024392e 100644 --- a/apps/desktop/src/locales/gl/messages.json +++ b/apps/desktop/src/locales/gl/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Continue" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/he/messages.json b/apps/desktop/src/locales/he/messages.json index 763401ac6fe..66654aafbc6 100644 --- a/apps/desktop/src/locales/he/messages.json +++ b/apps/desktop/src/locales/he/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "קוד אימות שגוי" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "המשך" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/hi/messages.json b/apps/desktop/src/locales/hi/messages.json index 33b69ac1519..d797b6319c0 100644 --- a/apps/desktop/src/locales/hi/messages.json +++ b/apps/desktop/src/locales/hi/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Continue" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/hr/messages.json b/apps/desktop/src/locales/hr/messages.json index 2dc081fa3c7..969a0df9cee 100644 --- a/apps/desktop/src/locales/hr/messages.json +++ b/apps/desktop/src/locales/hr/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Nevažeći kôd za provjeru" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Nastavi" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/hu/messages.json b/apps/desktop/src/locales/hu/messages.json index 3ec097c2a7a..d73d746fded 100644 --- a/apps/desktop/src/locales/hu/messages.json +++ b/apps/desktop/src/locales/hu/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Érvénytelen ellenőrző kód" }, + "invalidEmailOrVerificationCode": { + "message": "Az email cím vagy az ellenőrző kód érvénytelen." + }, "continue": { "message": "Folytatás" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Írjunk be több email címet vesszővel elválasztva." }, + "emailsRequiredChangeAccessType": { + "message": "Az email cím ellenőrzéshez legalább egy email cím szükséges. Az összes email cím eltávolításához módosítsuk a fenti hozzáférési típust." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "A felhasználó ellenőrzése sikertelen volt." } } diff --git a/apps/desktop/src/locales/id/messages.json b/apps/desktop/src/locales/id/messages.json index 7648f4bb99b..f5c74c471de 100644 --- a/apps/desktop/src/locales/id/messages.json +++ b/apps/desktop/src/locales/id/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Kode verifikasi tidak valid" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Lanjutkan" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/it/messages.json b/apps/desktop/src/locales/it/messages.json index eb2ade245a0..c394dd84a6f 100644 --- a/apps/desktop/src/locales/it/messages.json +++ b/apps/desktop/src/locales/it/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Codice di verifica non valido" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Continua" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/ja/messages.json b/apps/desktop/src/locales/ja/messages.json index a9b05f728d8..908fa271a16 100644 --- a/apps/desktop/src/locales/ja/messages.json +++ b/apps/desktop/src/locales/ja/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "認証コードが間違っています" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "続行" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/ka/messages.json b/apps/desktop/src/locales/ka/messages.json index 68bba7fcb27..b9fb3f528d7 100644 --- a/apps/desktop/src/locales/ka/messages.json +++ b/apps/desktop/src/locales/ka/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "არასწორი გადამოწმების კოდი" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "გაგრძელება" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/km/messages.json b/apps/desktop/src/locales/km/messages.json index 6d0922bd680..3b80024392e 100644 --- a/apps/desktop/src/locales/km/messages.json +++ b/apps/desktop/src/locales/km/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Continue" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/kn/messages.json b/apps/desktop/src/locales/kn/messages.json index 3c6aced3a73..13463f63da1 100644 --- a/apps/desktop/src/locales/kn/messages.json +++ b/apps/desktop/src/locales/kn/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "ಮುಂದುವರಿಸಿ" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/ko/messages.json b/apps/desktop/src/locales/ko/messages.json index 8932f0efb48..524d1bc8b01 100644 --- a/apps/desktop/src/locales/ko/messages.json +++ b/apps/desktop/src/locales/ko/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "유효하지 않은 확인 코드" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "계속" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/lt/messages.json b/apps/desktop/src/locales/lt/messages.json index a19856a776e..3a003cb565e 100644 --- a/apps/desktop/src/locales/lt/messages.json +++ b/apps/desktop/src/locales/lt/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Neteisingas patvirtinimo kodas" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Tęsti" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/lv/messages.json b/apps/desktop/src/locales/lv/messages.json index 8a863256ed1..b8a0cdd8434 100644 --- a/apps/desktop/src/locales/lv/messages.json +++ b/apps/desktop/src/locales/lv/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Nederīgs apliecinājuma kods" }, + "invalidEmailOrVerificationCode": { + "message": "Nederīga e-pasta adrese vai apliecinājuma kods" + }, "continue": { "message": "Turpināt" }, @@ -4388,10 +4391,10 @@ "message": "Šeit parādīsies arhivētie vienumi, un tie netiks iekļauti vispārējās meklēšanas iznākumos un automātiskās aizpildes ieteikumos." }, "itemArchiveToast": { - "message": "Item archived" + "message": "Vienums ievietots arhīvā" }, "itemUnarchivedToast": { - "message": "Item unarchived" + "message": "Vienums izņemts no arhīva" }, "archiveItem": { "message": "Arhivēt vienumu" @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "E-pasta apliecināšanai ir nepieciešama vismaz viena e-pasta adrese. Lai noņemtu visas e-pasta adreses, augstāk jānomaina piekļūšanas veids." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "Lietotāja apliecināšana neizdevās." } } diff --git a/apps/desktop/src/locales/me/messages.json b/apps/desktop/src/locales/me/messages.json index 773a596a10d..de1c690bb8e 100644 --- a/apps/desktop/src/locales/me/messages.json +++ b/apps/desktop/src/locales/me/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Nastavi" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/ml/messages.json b/apps/desktop/src/locales/ml/messages.json index 3bddc3baa5b..0b15dafd31b 100644 --- a/apps/desktop/src/locales/ml/messages.json +++ b/apps/desktop/src/locales/ml/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "തുടരുക" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/mr/messages.json b/apps/desktop/src/locales/mr/messages.json index 6d0922bd680..3b80024392e 100644 --- a/apps/desktop/src/locales/mr/messages.json +++ b/apps/desktop/src/locales/mr/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Continue" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/my/messages.json b/apps/desktop/src/locales/my/messages.json index 22f4a30329a..6efe4072ecb 100644 --- a/apps/desktop/src/locales/my/messages.json +++ b/apps/desktop/src/locales/my/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Continue" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/nb/messages.json b/apps/desktop/src/locales/nb/messages.json index b2b1631fe04..a4842046c16 100644 --- a/apps/desktop/src/locales/nb/messages.json +++ b/apps/desktop/src/locales/nb/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Ugyldig verifiseringskode" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Fortsett" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/ne/messages.json b/apps/desktop/src/locales/ne/messages.json index b9eba55d8bd..43a96999ed3 100644 --- a/apps/desktop/src/locales/ne/messages.json +++ b/apps/desktop/src/locales/ne/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Continue" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/nl/messages.json b/apps/desktop/src/locales/nl/messages.json index 306bf31efe2..f6908fd6498 100644 --- a/apps/desktop/src/locales/nl/messages.json +++ b/apps/desktop/src/locales/nl/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Ongeldige verificatiecode" }, + "invalidEmailOrVerificationCode": { + "message": "Ongeldig e-mailadres verificatiecode" + }, "continue": { "message": "Doorgaan" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Voer meerdere e-mailadressen in door te scheiden met een komma." }, + "emailsRequiredChangeAccessType": { + "message": "E-mailverificatie vereist ten minste één e-mailadres. Om alle e-mailadressen te verwijderen, moet je het toegangstype hierboven wijzigen." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "Gebruikersverificatie is mislukt." } } diff --git a/apps/desktop/src/locales/nn/messages.json b/apps/desktop/src/locales/nn/messages.json index dc62d73a236..1c0ab7c51a8 100644 --- a/apps/desktop/src/locales/nn/messages.json +++ b/apps/desktop/src/locales/nn/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Ugyldig stadfestingskode" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Fortsett" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/or/messages.json b/apps/desktop/src/locales/or/messages.json index e8ea506a873..f28ba8ad5a0 100644 --- a/apps/desktop/src/locales/or/messages.json +++ b/apps/desktop/src/locales/or/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Continue" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/pl/messages.json b/apps/desktop/src/locales/pl/messages.json index 0cee8d6683d..e429217c278 100644 --- a/apps/desktop/src/locales/pl/messages.json +++ b/apps/desktop/src/locales/pl/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Kod weryfikacyjny jest nieprawidłowy" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Kontynuuj" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/pt_BR/messages.json b/apps/desktop/src/locales/pt_BR/messages.json index 817e9de0c50..1ec4807e2a4 100644 --- a/apps/desktop/src/locales/pt_BR/messages.json +++ b/apps/desktop/src/locales/pt_BR/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Código de verificação inválido" }, + "invalidEmailOrVerificationCode": { + "message": "E-mail ou código de verificação inválido" + }, "continue": { "message": "Continuar" }, @@ -4388,10 +4391,10 @@ "message": "Os itens arquivados aparecerão aqui e serão excluídos dos resultados gerais de busca e das sugestões de preenchimento automático." }, "itemArchiveToast": { - "message": "Item archived" + "message": "Item arquivado" }, "itemUnarchivedToast": { - "message": "Item unarchived" + "message": "Item desarquivado" }, "archiveItem": { "message": "Arquivar item" @@ -4487,7 +4490,7 @@ "message": "Ação do limite de tempo" }, "errorCannotDecrypt": { - "message": "Error: Cannot decrypt" + "message": "Erro: Não é possível descriptografar" }, "sessionTimeoutHeader": { "message": "Limite de tempo da sessão" @@ -4588,11 +4591,11 @@ "message": "Por que estou vendo isso?" }, "sendPasswordHelperText": { - "message": "Indivíduos precisarão utilizar a senha para ver este Send", + "message": "Os indivíduos precisarão digitar a senha para ver este Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "emailProtected": { - "message": "E-mail protegido" + "message": "Protegido por e-mail" }, "emails": { "message": "E-mails" @@ -4601,7 +4604,7 @@ "message": "Qualquer um com o link" }, "anyOneWithPassword": { - "message": "Qualquer um com uma senha definida por você" + "message": "Qualquer pessoa com uma senha configurada por você" }, "whoCanView": { "message": "Quem pode visualizar" @@ -4613,9 +4616,15 @@ "message": "Após compartilhar este link de Send, indivíduos precisarão verificar seus e-mails com um código para visualizar este Send." }, "enterMultipleEmailsSeparatedByComma": { - "message": "Insira múltiplos e-mails separando-os com vírgula." + "message": "Digite vários e-mails, separados com uma vírgula." + }, + "emailsRequiredChangeAccessType": { + "message": "A verificação de e-mail requer pelo menos um endereço de e-mail. Para remover todos, altere o tipo de acesso acima." }, "emailPlaceholder": { - "message": "user@bitwarden.com , user@acme.com" + "message": "usuário@bitwarden.com , usuário@acme.com" + }, + "userVerificationFailed": { + "message": "Falha na verificação do usuário." } } diff --git a/apps/desktop/src/locales/pt_PT/messages.json b/apps/desktop/src/locales/pt_PT/messages.json index 3076291a4a3..ca5091ccbe6 100644 --- a/apps/desktop/src/locales/pt_PT/messages.json +++ b/apps/desktop/src/locales/pt_PT/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Código de verificação inválido" }, + "invalidEmailOrVerificationCode": { + "message": "E-mail ou código de verificação inválido" + }, "continue": { "message": "Continuar" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Introduza vários e-mails, separados por vírgula." }, + "emailsRequiredChangeAccessType": { + "message": "A verificação por e-mail requer pelo menos um endereço de e-mail. Para remover todos os e-mails, altere o tipo de acesso acima." + }, "emailPlaceholder": { "message": "utilizador@bitwarden.com , utilizador@acme.com" + }, + "userVerificationFailed": { + "message": "Falha na verificação do utilizador." } } diff --git a/apps/desktop/src/locales/ro/messages.json b/apps/desktop/src/locales/ro/messages.json index b8fc25b4105..6c67e9d9e06 100644 --- a/apps/desktop/src/locales/ro/messages.json +++ b/apps/desktop/src/locales/ro/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Cod de verificare nevalid" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Continuare" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/ru/messages.json b/apps/desktop/src/locales/ru/messages.json index 089e815fefe..e48319dac55 100644 --- a/apps/desktop/src/locales/ru/messages.json +++ b/apps/desktop/src/locales/ru/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Неверный код подтверждения" }, + "invalidEmailOrVerificationCode": { + "message": "Неверный email или код подтверждения" + }, "continue": { "message": "Продолжить" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Введите несколько email, разделяя их запятой." }, + "emailsRequiredChangeAccessType": { + "message": "Для проверки электронной почты требуется как минимум один адрес email. Чтобы удалить все адреса электронной почты, измените тип доступа выше." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "Проверка пользователя не удалась." } } diff --git a/apps/desktop/src/locales/si/messages.json b/apps/desktop/src/locales/si/messages.json index a1a84b8ba15..4d75c329656 100644 --- a/apps/desktop/src/locales/si/messages.json +++ b/apps/desktop/src/locales/si/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "ඉදිරියට" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/sk/messages.json b/apps/desktop/src/locales/sk/messages.json index 2876361bbf0..50d88a49405 100644 --- a/apps/desktop/src/locales/sk/messages.json +++ b/apps/desktop/src/locales/sk/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Neplatný verifikačný kód" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Pokračovať" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Zadajte viacero e-mailových adries oddelených čiarkou." }, + "emailsRequiredChangeAccessType": { + "message": "Overenie e-mailu vyžaduje aspoň jednu e-mailovú adresu. Ak chcete odstrániť všetky e-maily, zmeňte typ prístupu vyššie." + }, "emailPlaceholder": { "message": "pouzivate@bitwarden.com, pouzivatel@acme.com" + }, + "userVerificationFailed": { + "message": "Overenie používateľa zlyhalo." } } diff --git a/apps/desktop/src/locales/sl/messages.json b/apps/desktop/src/locales/sl/messages.json index 82e0a20b29e..63ae05a12b5 100644 --- a/apps/desktop/src/locales/sl/messages.json +++ b/apps/desktop/src/locales/sl/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Neveljavna verifikacijska koda" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Nadaljuj" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/sr/messages.json b/apps/desktop/src/locales/sr/messages.json index 633d3123242..32715ed60d1 100644 --- a/apps/desktop/src/locales/sr/messages.json +++ b/apps/desktop/src/locales/sr/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Неисправан верификациони код" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Настави" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/sv/messages.json b/apps/desktop/src/locales/sv/messages.json index ed9e62e8319..b44d54a14a3 100644 --- a/apps/desktop/src/locales/sv/messages.json +++ b/apps/desktop/src/locales/sv/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Ogiltig verifieringskod" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Fortsätt" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Ange flera e-postadresser genom att separera dem med kommatecken." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "användare@bitwarden.com , användare@acme.com" + }, + "userVerificationFailed": { + "message": "Verifiering av användare misslyckades." } } diff --git a/apps/desktop/src/locales/ta/messages.json b/apps/desktop/src/locales/ta/messages.json index 53e155874f6..feb4b92c77e 100644 --- a/apps/desktop/src/locales/ta/messages.json +++ b/apps/desktop/src/locales/ta/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "தவறான சரிபார்ப்புக் குறியீடு" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "தொடரவும்" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/te/messages.json b/apps/desktop/src/locales/te/messages.json index 6d0922bd680..3b80024392e 100644 --- a/apps/desktop/src/locales/te/messages.json +++ b/apps/desktop/src/locales/te/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Continue" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/th/messages.json b/apps/desktop/src/locales/th/messages.json index b7117a7ccad..eeb0029b928 100644 --- a/apps/desktop/src/locales/th/messages.json +++ b/apps/desktop/src/locales/th/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "รหัสการตรวจสอบสิทธิ์ไม่ถูกต้อง" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "ดำเนินการต่อไป" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/tr/messages.json b/apps/desktop/src/locales/tr/messages.json index 96bb11c7a35..29171ca8fef 100644 --- a/apps/desktop/src/locales/tr/messages.json +++ b/apps/desktop/src/locales/tr/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Geçersiz doğrulama kodu" }, + "invalidEmailOrVerificationCode": { + "message": "E-posta veya doğrulama kodu geçersiz" + }, "continue": { "message": "Devam Et" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "E-posta adreslerini virgülle ayırarak yazın." }, + "emailsRequiredChangeAccessType": { + "message": "E-posta doğrulaması için en az bir e-posta adresi gerekir. Tüm e-postaları silmek için yukarıdan erişim türünü değiştirin." + }, "emailPlaceholder": { "message": "kullanici@bitwarden.com , kullanici@acme.com" + }, + "userVerificationFailed": { + "message": "Kullanıcı doğrulaması başarısız oldu." } } diff --git a/apps/desktop/src/locales/uk/messages.json b/apps/desktop/src/locales/uk/messages.json index abcdbea0b1f..e982d46b454 100644 --- a/apps/desktop/src/locales/uk/messages.json +++ b/apps/desktop/src/locales/uk/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Недійсний код підтвердження" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Продовжити" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/vi/messages.json b/apps/desktop/src/locales/vi/messages.json index f06556568a1..8fe0adee948 100644 --- a/apps/desktop/src/locales/vi/messages.json +++ b/apps/desktop/src/locales/vi/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Mã xác minh không đúng" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Tiếp tục" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/zh_CN/messages.json b/apps/desktop/src/locales/zh_CN/messages.json index 9941e296da3..ad09c8f032e 100644 --- a/apps/desktop/src/locales/zh_CN/messages.json +++ b/apps/desktop/src/locales/zh_CN/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "无效的验证码" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "继续" }, @@ -4388,10 +4391,10 @@ "message": "已归档的项目将显示在此处,并将被排除在一般搜索结果和自动填充建议之外。" }, "itemArchiveToast": { - "message": "Item archived" + "message": "项目已归档" }, "itemUnarchivedToast": { - "message": "Item unarchived" + "message": "项目已取消归档" }, "archiveItem": { "message": "归档项目" @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "输入多个电子邮箱(使用逗号分隔)。" }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com, user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/zh_TW/messages.json b/apps/desktop/src/locales/zh_TW/messages.json index 461eb031068..364f67c7b58 100644 --- a/apps/desktop/src/locales/zh_TW/messages.json +++ b/apps/desktop/src/locales/zh_TW/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "無效的驗證碼" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "繼續" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "請以逗號分隔輸入多個電子郵件地址。" }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } From 2f6a5133f8c53d345c8cd133a722578813e09963 Mon Sep 17 00:00:00 2001 From: "bw-ghapp[bot]" <178206702+bw-ghapp[bot]@users.noreply.github.com> Date: Fri, 20 Feb 2026 13:24:40 +0100 Subject: [PATCH 39/89] Autosync the updated translations (#19094) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/browser/src/_locales/ar/messages.json | 51 +++++ apps/browser/src/_locales/az/messages.json | 51 +++++ apps/browser/src/_locales/be/messages.json | 51 +++++ apps/browser/src/_locales/bg/messages.json | 51 +++++ apps/browser/src/_locales/bn/messages.json | 51 +++++ apps/browser/src/_locales/bs/messages.json | 51 +++++ apps/browser/src/_locales/ca/messages.json | 51 +++++ apps/browser/src/_locales/cs/messages.json | 51 +++++ apps/browser/src/_locales/cy/messages.json | 51 +++++ apps/browser/src/_locales/da/messages.json | 51 +++++ apps/browser/src/_locales/de/messages.json | 51 +++++ apps/browser/src/_locales/el/messages.json | 51 +++++ apps/browser/src/_locales/en_GB/messages.json | 51 +++++ apps/browser/src/_locales/en_IN/messages.json | 51 +++++ apps/browser/src/_locales/es/messages.json | 51 +++++ apps/browser/src/_locales/et/messages.json | 51 +++++ apps/browser/src/_locales/eu/messages.json | 51 +++++ apps/browser/src/_locales/fa/messages.json | 51 +++++ apps/browser/src/_locales/fi/messages.json | 91 ++++++-- apps/browser/src/_locales/fil/messages.json | 51 +++++ apps/browser/src/_locales/fr/messages.json | 165 +++++++++----- apps/browser/src/_locales/gl/messages.json | 51 +++++ apps/browser/src/_locales/he/messages.json | 51 +++++ apps/browser/src/_locales/hi/messages.json | 51 +++++ apps/browser/src/_locales/hr/messages.json | 57 ++++- apps/browser/src/_locales/hu/messages.json | 51 +++++ apps/browser/src/_locales/id/messages.json | 51 +++++ apps/browser/src/_locales/it/messages.json | 51 +++++ apps/browser/src/_locales/ja/messages.json | 51 +++++ apps/browser/src/_locales/ka/messages.json | 51 +++++ apps/browser/src/_locales/km/messages.json | 51 +++++ apps/browser/src/_locales/kn/messages.json | 51 +++++ apps/browser/src/_locales/ko/messages.json | 51 +++++ apps/browser/src/_locales/lt/messages.json | 51 +++++ apps/browser/src/_locales/lv/messages.json | 55 ++++- apps/browser/src/_locales/ml/messages.json | 51 +++++ apps/browser/src/_locales/mr/messages.json | 51 +++++ apps/browser/src/_locales/my/messages.json | 51 +++++ apps/browser/src/_locales/nb/messages.json | 51 +++++ apps/browser/src/_locales/ne/messages.json | 51 +++++ apps/browser/src/_locales/nl/messages.json | 51 +++++ apps/browser/src/_locales/nn/messages.json | 51 +++++ apps/browser/src/_locales/or/messages.json | 51 +++++ apps/browser/src/_locales/pl/messages.json | 51 +++++ apps/browser/src/_locales/pt_BR/messages.json | 83 +++++-- apps/browser/src/_locales/pt_PT/messages.json | 51 +++++ apps/browser/src/_locales/ro/messages.json | 51 +++++ apps/browser/src/_locales/ru/messages.json | 51 +++++ apps/browser/src/_locales/si/messages.json | 51 +++++ apps/browser/src/_locales/sk/messages.json | 51 +++++ apps/browser/src/_locales/sl/messages.json | 209 +++++++++++------- apps/browser/src/_locales/sr/messages.json | 51 +++++ apps/browser/src/_locales/sv/messages.json | 51 +++++ apps/browser/src/_locales/ta/messages.json | 51 +++++ apps/browser/src/_locales/te/messages.json | 51 +++++ apps/browser/src/_locales/th/messages.json | 51 +++++ apps/browser/src/_locales/tr/messages.json | 51 +++++ apps/browser/src/_locales/uk/messages.json | 51 +++++ apps/browser/src/_locales/vi/messages.json | 51 +++++ apps/browser/src/_locales/zh_CN/messages.json | 55 ++++- apps/browser/src/_locales/zh_TW/messages.json | 51 +++++ apps/browser/store/locales/sl/copy.resx | 6 +- 62 files changed, 3293 insertions(+), 182 deletions(-) diff --git a/apps/browser/src/_locales/ar/messages.json b/apps/browser/src/_locales/ar/messages.json index 7334362d446..5768966511d 100644 --- a/apps/browser/src/_locales/ar/messages.json +++ b/apps/browser/src/_locales/ar/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "رمز التحقق غير صالح" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "تم نسخ $VALUE$", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration of a list of logins that are at-risk." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "إرسال رابط منسوخ", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/az/messages.json b/apps/browser/src/_locales/az/messages.json index 6168f7cf2dd..6572c2d09d9 100644 --- a/apps/browser/src/_locales/az/messages.json +++ b/apps/browser/src/_locales/az/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Yararsız doğrulama kodu" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ kopyalandı", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Risk altındakı girişlərin olduğu siyahının təsviri." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Risk altında olan saytda Bitwarden avto-doldurma menyusu ilə güclü, unikal parolları cəld yaradın.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send keçidi kopyalandı", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Birdən çox e-poçtu daxil edərkən vergül istifadə edin." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Şəxslər, Send-ə baxması üçün parolu daxil etməli olacaqlar", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/be/messages.json b/apps/browser/src/_locales/be/messages.json index ea569cabdf4..cd48ff09ee4 100644 --- a/apps/browser/src/_locales/be/messages.json +++ b/apps/browser/src/_locales/be/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Памылковы праверачны код" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ скапіяваны", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration of a list of logins that are at-risk." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send link copied", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/bg/messages.json b/apps/browser/src/_locales/bg/messages.json index e5d68bce366..4f2f26bade8 100644 --- a/apps/browser/src/_locales/bg/messages.json +++ b/apps/browser/src/_locales/bg/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Грешен код за потвърждаване" }, + "invalidEmailOrVerificationCode": { + "message": "Грешна е-поща или код за потвърждаване" + }, "valueCopied": { "message": "Копирано е $VALUE$", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Илюстрация на списък с елементи за вписване, които са в риск." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Генерирайте бързо сложна и уникална парола от менюто за автоматично попълване на Битуорден, на уеб сайта, който е в риск.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ часа", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Копирайте и споделете връзката към Изпращането. То ще бъде достъпно за всеки с връзката в рамките на следващите $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Копирайте и споделете връзката към Изпращането. То ще бъде достъпно за всеки с връзката и паролата, която зададете, в рамките на следващите $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Копирайте и споделете връзка към това Изпращане. То ще може да бъде видяно само от хората, които сте посочили, в рамките на следващите $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Връзката към Изпращането е копирана", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Можете да въведете повече е-пощи, като ги разделите със запетая." }, + "emailsRequiredChangeAccessType": { + "message": "Потвърждаването на е-пощата изисква да е наличен поне един адрес на е-поща. Ако искате да премахнете всички е-пощи, променете начина за достъп по-горе." + }, "emailPlaceholder": { "message": "потребител@bitwarden.com , потребител@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Хората ще трябва да въведат паролата, за да видят това Изпращане", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "Проверката на потребителя беше неуспешна." } } diff --git a/apps/browser/src/_locales/bn/messages.json b/apps/browser/src/_locales/bn/messages.json index 533b12ab0a5..8aa07e2ec82 100644 --- a/apps/browser/src/_locales/bn/messages.json +++ b/apps/browser/src/_locales/bn/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ অনুলিপিত", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration of a list of logins that are at-risk." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send link copied", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/bs/messages.json b/apps/browser/src/_locales/bs/messages.json index 35c4177e5eb..8ef61e0e63e 100644 --- a/apps/browser/src/_locales/bs/messages.json +++ b/apps/browser/src/_locales/bs/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ copied", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration of a list of logins that are at-risk." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send link copied", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/ca/messages.json b/apps/browser/src/_locales/ca/messages.json index 8e82fc34be4..d524731fb46 100644 --- a/apps/browser/src/_locales/ca/messages.json +++ b/apps/browser/src/_locales/ca/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Codi de verificació no vàlid" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "S'ha copiat $VALUE$", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration of a list of logins that are at-risk." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send link copied", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/cs/messages.json b/apps/browser/src/_locales/cs/messages.json index ed1b37134e1..9106d0518db 100644 --- a/apps/browser/src/_locales/cs/messages.json +++ b/apps/browser/src/_locales/cs/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Neplatný ověřovací kód" }, + "invalidEmailOrVerificationCode": { + "message": "Neplatný e-mail nebo ověřovací kód" + }, "valueCopied": { "message": "Zkopírováno: $VALUE$", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Ilustrace seznamu přihlášení, která jsou ohrožená." }, + "welcomeDialogGraphicAlt": { + "message": "Ilustrace rozložení stránky trezoru Bitwarden." + }, "generatePasswordSlideDesc": { "message": "Rychle vygeneruje silné, unikátní heslo s nabídkou automatického vyplňování Bitwarden na ohrožených stránkách.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hodin", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Zkopírujte a sdílejte tento odkaz Send. Send bude k dispozici komukoli s odkazem na dalších $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Zkopírujte a sdílejte tento odkaz Send. Send bude k dispozici komukoli s odkazem a heslem na dalších $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Zkopírujte a sdílejte tento Send pro odesílání. Můžou jej zobrazit osoby, které jste zadali, a to po dobu $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Odkaz Send byl zkopírován", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Zadejte více e-mailů oddělených čárkou." }, + "emailsRequiredChangeAccessType": { + "message": "Ověření e-mailu vyžaduje alespoň jednu e-mailovou adresu. Chcete-li odebrat všechny emaily, změňte výše uvedený typ přístupu." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Pro zobrazení tohoto Send budou muset jednotlivci zadat heslo", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "Ověření uživatele se nezdařilo." } } diff --git a/apps/browser/src/_locales/cy/messages.json b/apps/browser/src/_locales/cy/messages.json index 165cd05de8e..12dc8d7b44f 100644 --- a/apps/browser/src/_locales/cy/messages.json +++ b/apps/browser/src/_locales/cy/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Cod dilysu annilys" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ wedi'i gopïo", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration of a list of logins that are at-risk." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send link copied", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/da/messages.json b/apps/browser/src/_locales/da/messages.json index 615cc6a2a0b..d19d07ee9f5 100644 --- a/apps/browser/src/_locales/da/messages.json +++ b/apps/browser/src/_locales/da/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Ugyldig bekræftelseskode" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ kopieret", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration of a list of logins that are at-risk." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send-link kopieret", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/de/messages.json b/apps/browser/src/_locales/de/messages.json index 8f2b023bc00..6d301950e03 100644 --- a/apps/browser/src/_locales/de/messages.json +++ b/apps/browser/src/_locales/de/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Ungültiger Verifizierungscode" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ kopiert", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration einer Liste gefährdeter Zugangsdaten." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Generiere schnell ein starkes, einzigartiges Passwort mit dem Bitwarden Auto-Ausfüllen-Menü auf der gefährdeten Website.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ Stunden", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Kopiere und teile diesen Send-Link. Das Send wird für jeden mit dem Link für die nächsten $TIME$ verfügbar sein.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Kopiere und teile diesen Send-Link. Das Send wird für jeden mit dem Link und dem von dir festgelegten Passwort für die nächsten $TIME$ verfügbar sein.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Kopiere und teile diesen Send-Link. Er kann von den von dir angegebenen Personen für die nächsten $TIME$ angesehen werden.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send-Link kopiert", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Gib mehrere E-Mail-Adressen ein, indem du sie mit einem Komma trennst." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "benutzer@bitwarden.com, benutzer@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Personen müssen das Passwort eingeben, um dieses Send anzusehen", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "Benutzerverifizierung fehlgeschlagen." } } diff --git a/apps/browser/src/_locales/el/messages.json b/apps/browser/src/_locales/el/messages.json index 68f7267825d..0e5fc2eaeb1 100644 --- a/apps/browser/src/_locales/el/messages.json +++ b/apps/browser/src/_locales/el/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Μη έγκυρος κωδικός επαλήθευσης" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ αντιγράφηκε", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration of a list of logins that are at-risk." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Ο σύνδεσμος Send αντιγράφηκε", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/en_GB/messages.json b/apps/browser/src/_locales/en_GB/messages.json index d61774df145..be564f8a950 100644 --- a/apps/browser/src/_locales/en_GB/messages.json +++ b/apps/browser/src/_locales/en_GB/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ copied", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration of a list of logins that are at-risk." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send link copied", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/en_IN/messages.json b/apps/browser/src/_locales/en_IN/messages.json index 3622ffce241..18e02ec48ca 100644 --- a/apps/browser/src/_locales/en_IN/messages.json +++ b/apps/browser/src/_locales/en_IN/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ copied", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration of a list of logins that are at-risk." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send link copied", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/es/messages.json b/apps/browser/src/_locales/es/messages.json index 131263ea4d9..45d2139fd6b 100644 --- a/apps/browser/src/_locales/es/messages.json +++ b/apps/browser/src/_locales/es/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Código de verificación no válido" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "Valor de $VALUE$ copiado", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration of a list of logins that are at-risk." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Genera rápidamente una contraseña segura y única con el menú de autocompletado de Bitwarden en el sitio en riesgo.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Enlace del Send copiado", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Introduce varios correos electrónicos separándolos con una coma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Los individuos tendrán que introducir la contraseña para ver este Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/et/messages.json b/apps/browser/src/_locales/et/messages.json index cd78c444c89..789454218e1 100644 --- a/apps/browser/src/_locales/et/messages.json +++ b/apps/browser/src/_locales/et/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Vale kinnituskood" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ on kopeeritud", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration of a list of logins that are at-risk." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send link copied", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/eu/messages.json b/apps/browser/src/_locales/eu/messages.json index 3e4382a3d3b..f08604910af 100644 --- a/apps/browser/src/_locales/eu/messages.json +++ b/apps/browser/src/_locales/eu/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Egiaztatze-kodea ez da baliozkoa" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ kopiatuta", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration of a list of logins that are at-risk." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send link copied", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/fa/messages.json b/apps/browser/src/_locales/fa/messages.json index 5bb22dc6292..4c2474d614c 100644 --- a/apps/browser/src/_locales/fa/messages.json +++ b/apps/browser/src/_locales/fa/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "کد تأیید نامعتبر است" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ کپی شد", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "تصویری از فهرست ورودهایی که در معرض خطر هستند." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "با استفاده از منوی پر کردن خودکار Bitwarden در سایت در معرض خطر، به‌سرعت یک کلمه عبور قوی و منحصر به فرد تولید کنید.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "لینک ارسال کپی شد", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/fi/messages.json b/apps/browser/src/_locales/fi/messages.json index 2f5b1ec4932..8f5d3c05a63 100644 --- a/apps/browser/src/_locales/fi/messages.json +++ b/apps/browser/src/_locales/fi/messages.json @@ -29,7 +29,7 @@ "message": "Kirjaudu pääsyavaimella" }, "unlockWithPasskey": { - "message": "Unlock with passkey" + "message": "Poista lukitus todentamisavaimella" }, "useSingleSignOn": { "message": "Käytä kertakirjautumista" @@ -440,7 +440,7 @@ "message": "Synkronointi" }, "syncNow": { - "message": "Sync now" + "message": "Synkronoi nyt" }, "lastSync": { "message": "Viimeisin synkronointi:" @@ -607,10 +607,10 @@ "message": "Näytä kaikki" }, "showAll": { - "message": "Show all" + "message": "Näytä kaikki" }, "viewLess": { - "message": "View less" + "message": "Näytä vähemmän" }, "viewLogin": { "message": "View login" @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Virheellinen todennuskoodi" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ kopioitu", "description": "Value has been copied to the clipboard.", @@ -1492,7 +1495,7 @@ "message": "This file is using an outdated encryption method." }, "attachmentUpdated": { - "message": "Attachment updated" + "message": "Liite päivitetty" }, "file": { "message": "Tiedosto" @@ -1661,7 +1664,7 @@ "message": "Passkey authentication failed" }, "useADifferentLogInMethod": { - "message": "Use a different log in method" + "message": "Käytä vaihtoehtoista kirjautumistapaa" }, "awaitingSecurityKeyInteraction": { "message": "Odotetaan suojausavaimen aktivointia..." @@ -1953,7 +1956,7 @@ "message": "Erääntymisvuosi" }, "monthly": { - "message": "month" + "message": "kuukausi" }, "expiration": { "message": "Voimassaolo päättyy" @@ -2052,7 +2055,7 @@ "message": "Sähköposti" }, "emails": { - "message": "Emails" + "message": "Sähköpostit" }, "phone": { "message": "Puhelinnumero" @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Kuvitus vaarantuneiden kirjautumistietojen luettelosta." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Luo vahva ja ainutlaatuinen salasana nopeasti Bitwardenin automaattitäytön valikosta vaarantuneella sivustolla.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send-linkki kopioitiin", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -4824,7 +4869,7 @@ "message": "Hallintapaneelista" }, "admin": { - "message": "Admin" + "message": "Ylläpitäjä" }, "automaticUserConfirmation": { "message": "Automatic user confirmation" @@ -4854,16 +4899,16 @@ "message": "Turned on automatic confirmation" }, "availableNow": { - "message": "Available now" + "message": "Saatavilla nyt" }, "accountSecurity": { "message": "Tilin suojaus" }, "phishingBlocker": { - "message": "Phishing Blocker" + "message": "Tietojenkalasteluhyökkäysten estäminen" }, "enablePhishingDetection": { - "message": "Phishing detection" + "message": "Tietojenkalasteluhyökkäysten tunnistaminen" }, "enablePhishingDetectionDesc": { "message": "Display warning before accessing suspected phishing sites" @@ -4981,7 +5026,7 @@ } }, "downloadAttachmentLabel": { - "message": "Download Attachment" + "message": "Lataa liite" }, "downloadBitwarden": { "message": "Lataa Bitwarden" @@ -5716,10 +5761,10 @@ "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, "vulnerablePassword": { - "message": "Vulnerable password." + "message": "Heikko salasana" }, "changeNow": { - "message": "Change now" + "message": "Vaihda nyt" }, "missingWebsite": { "message": "Missing website" @@ -5773,13 +5818,13 @@ "message": "Tervetuloa holviisi!" }, "phishingPageTitleV2": { - "message": "Phishing attempt detected" + "message": "Havaittu tietojenkalasteluhyökkäyksen yritys" }, "phishingPageSummary": { - "message": "The site you are attempting to visit is a known malicious site and a security risk." + "message": "Sivusto jota olet avaamassa on tunnetusti haitallinen ja sen avaaminen on turvallisuusriski" }, "phishingPageCloseTabV2": { - "message": "Close this tab" + "message": "Sulje tämä välilehti" }, "phishingPageContinueV2": { "message": "Jatka tälle sivustolle (ei suositeltavaa)" @@ -5893,10 +5938,10 @@ "description": "'WebAssembly' is a technical term and should not be translated." }, "showMore": { - "message": "Show more" + "message": "Näytä enemmän" }, "showLess": { - "message": "Show less" + "message": "Näytä vähemmän" }, "next": { "message": "Seuraava" @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/fil/messages.json b/apps/browser/src/_locales/fil/messages.json index abb06f0f19f..069be19dc86 100644 --- a/apps/browser/src/_locales/fil/messages.json +++ b/apps/browser/src/_locales/fil/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Maling verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "Kinopya ang $VALUE$", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration of a list of logins that are at-risk." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send link copied", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/fr/messages.json b/apps/browser/src/_locales/fr/messages.json index 596315c4d3f..afd5415e249 100644 --- a/apps/browser/src/_locales/fr/messages.json +++ b/apps/browser/src/_locales/fr/messages.json @@ -29,7 +29,7 @@ "message": "Se connecter avec une clé d'accès" }, "unlockWithPasskey": { - "message": "Unlock with passkey" + "message": "Déverrouiller avec une clé d'accès" }, "useSingleSignOn": { "message": "Utiliser l'authentification unique" @@ -574,28 +574,28 @@ "message": "Les éléments archivés apparaîtront ici et seront exclus des résultats de recherche généraux et des suggestions de remplissage automatique." }, "itemArchiveToast": { - "message": "Item archived" + "message": "Élément archivé" }, "itemUnarchivedToast": { - "message": "Item unarchived" + "message": "Élément désarchivé" }, "archiveItem": { "message": "Archiver l'élément" }, "archiveItemDialogContent": { - "message": "Once archived, this item will be excluded from search results and autofill suggestions." + "message": "Une fois archivé, cet élément sera exclu des résultats de recherche et des suggestions de remplissage automatique." }, "archived": { - "message": "Archived" + "message": "Archivé" }, "unarchiveAndSave": { - "message": "Unarchive and save" + "message": "Désarchiver et enregistrer" }, "upgradeToUseArchive": { "message": "Une adhésion premium est requise pour utiliser Archive." }, "itemRestored": { - "message": "Item has been restored" + "message": "L'élément a été restauré" }, "edit": { "message": "Modifier" @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Code de vérification invalide" }, + "invalidEmailOrVerificationCode": { + "message": "Courriel ou code de vérification invalide" + }, "valueCopied": { "message": "$VALUE$ copié", "description": "Value has been copied to the clipboard.", @@ -988,10 +991,10 @@ "message": "Non" }, "noAuth": { - "message": "Anyone with the link" + "message": "Toute personne disposant du lien" }, "anyOneWithPassword": { - "message": "Anyone with a password set by you" + "message": "N'importe qui avec un mot de passe défini par vous" }, "location": { "message": "Emplacement" @@ -1318,7 +1321,7 @@ "description": "Default URI match detection for autofill." }, "defaultUriMatchDetectionDesc": { - "message": "Choisit la manière dont la détection des correspondances URI est gérée par défaut pour les connexions lors d'actions telles que la saisie automatique." + "message": "Choisissez le mode de traitement par défaut de la détection de correspondance URI, pour les connexions lors de l'exécution d'actions telles que le remplissage automatique." }, "theme": { "message": "Thème" @@ -1341,7 +1344,7 @@ "message": "Exporter à partir de" }, "exportVerb": { - "message": "Export", + "message": "Exporter", "description": "The verb form of the word Export" }, "exportNoun": { @@ -1353,7 +1356,7 @@ "description": "The noun form of the word Import" }, "importVerb": { - "message": "Import", + "message": "Importer", "description": "The verb form of the word Import" }, "fileFormat": { @@ -1552,13 +1555,13 @@ "message": "Options de connexion propriétaires à deux facteurs telles que YubiKey et Duo." }, "premiumSubscriptionEnded": { - "message": "Your Premium subscription ended" + "message": "Votre abonnement Premium est terminé" }, "archivePremiumRestart": { - "message": "To regain access to your archive, restart your Premium subscription. If you edit details for an archived item before restarting, it'll be moved back into your vault." + "message": "Pour récupérer l'accès à vos archives, redémarrez votre abonnement Premium. Si vous modifiez les détails d'un élément archivé avant de le redémarrer, il sera déplacé dans votre coffre." }, "restartPremium": { - "message": "Restart Premium" + "message": "Redémarrer Premium" }, "ppremiumSignUpReports": { "message": "Hygiène du mot de passe, santé du compte et rapports sur les brèches de données pour assurer la sécurité de votre coffre." @@ -2052,7 +2055,7 @@ "message": "Courriel" }, "emails": { - "message": "Emails" + "message": "Courriels" }, "phone": { "message": "Téléphone" @@ -2483,7 +2486,7 @@ "message": "Élément définitivement supprimé" }, "archivedItemRestored": { - "message": "Archived item restored" + "message": "Élément archivé restauré" }, "restoreItem": { "message": "Restaurer l'élément" @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration d'une liste de connexions à risque." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Générez rapidement un mot de passe fort et unique grâce au menu de saisie automatique de Bitwarden sur le site à risque.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ heures", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copiez et partagez ce lien Send. Le Send sera accessible à toute personne disposant du lien pour les prochains $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copiez et partagez ce lien Send. Le Send sera accessible à toute personne possédant le lien et le mot de passe que vous avez défini pendant $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copiez et partagez ce lien Send. Il peut être vu par les personnes définies pendant $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Lien Send copié", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -3350,10 +3395,10 @@ "message": "Erreur" }, "prfUnlockFailed": { - "message": "Failed to unlock with passkey. Please try again or use another unlock method." + "message": "Impossible de déverrouiller avec la clé d'accès. Veuillez réessayer ou utiliser une autre méthode de déverrouillage." }, "noPrfCredentialsAvailable": { - "message": "No PRF-enabled passkeys are available for unlock. Please log in with a passkey first." + "message": "Aucune clé d'accès PRF disponible pour le déverrouillage. Veuillez vous connecter avec une clé d'accès en premier lieu." }, "decryptionError": { "message": "Erreur de déchiffrement" @@ -4142,7 +4187,7 @@ "message": "Ok" }, "toggleSideNavigation": { - "message": "Basculer la navigation latérale" + "message": "Activer la navigation latérale" }, "skipToContent": { "message": "Accéder directement au contenu" @@ -4731,7 +4776,7 @@ } }, "moreOptionsLabelNoPlaceholder": { - "message": "More options" + "message": "Plus de paramètres" }, "moreOptionsTitle": { "message": "Plus d'options - $ITEMNAME$", @@ -4827,46 +4872,46 @@ "message": "Admin" }, "automaticUserConfirmation": { - "message": "Automatic user confirmation" + "message": "Confirmation d'utilisateur automatique" }, "automaticUserConfirmationHint": { - "message": "Automatically confirm pending users while this device is unlocked" + "message": "Confirmer automatiquement les utilisateurs en attente pendant que cet appareil est déverrouillé" }, "autoConfirmOnboardingCallout": { - "message": "Save time with automatic user confirmation" + "message": "Gagnez du temps grâce à la confirmation d'utilisateur automatique" }, "autoConfirmWarning": { - "message": "This could impact your organization’s data security. " + "message": "Cela peut avoir un impact sur la sécurité des données de votre organisation. " }, "autoConfirmWarningLink": { - "message": "Learn about the risks" + "message": "En apprendre plus sur les risques" }, "autoConfirmSetup": { - "message": "Automatically confirm new users" + "message": "Confirmer les nouveaux utilisateurs automatiquement" }, "autoConfirmSetupDesc": { - "message": "New users will be automatically confirmed while this device is unlocked." + "message": "Les nouveaux utilisateurs seront confirmés automatiquement pendant que cet appareil est déverrouillé." }, "autoConfirmSetupHint": { - "message": "What are the potential security risks?" + "message": "Quels-sont les potentiels risques de sécurité ?" }, "autoConfirmEnabled": { - "message": "Turned on automatic confirmation" + "message": "Confirmation automatique activée" }, "availableNow": { - "message": "Available now" + "message": "Disponible maintenant" }, "accountSecurity": { "message": "Sécurité du compte" }, "phishingBlocker": { - "message": "Phishing Blocker" + "message": "Bloqueur d'hameçonnage" }, "enablePhishingDetection": { - "message": "Phishing detection" + "message": "Détection de l'hameçonnage" }, "enablePhishingDetectionDesc": { - "message": "Display warning before accessing suspected phishing sites" + "message": "Afficher un avertissement avant d'accéder à des sites soupçonnés d'hameçonnage" }, "notifications": { "message": "Notifications" @@ -4981,7 +5026,7 @@ } }, "downloadAttachmentLabel": { - "message": "Download Attachment" + "message": "Télécharger la pièce jointe" }, "downloadBitwarden": { "message": "Télécharger Bitwarden" @@ -5123,10 +5168,10 @@ } }, "showMatchDetectionNoPlaceholder": { - "message": "Show match detection" + "message": "Afficher la détection de correspondance" }, "hideMatchDetectionNoPlaceholder": { - "message": "Hide match detection" + "message": "Masquer la détection de correspondance" }, "autoFillOnPageLoad": { "message": "Saisir automatiquement lors du chargement de la page ?" @@ -5362,10 +5407,10 @@ "message": "Emplacement de l'élément" }, "fileSends": { - "message": "Envoi de fichiers" + "message": "Sends de fichier" }, "textSends": { - "message": "Envoi de textes" + "message": "Sends de texte" }, "accountActions": { "message": "Actions du compte" @@ -5665,7 +5710,7 @@ "message": "Très large" }, "narrow": { - "message": "Narrow" + "message": "Réduire" }, "sshKeyWrongPassword": { "message": "Le mot de passe saisi est incorrect." @@ -5716,10 +5761,10 @@ "message": "Cet identifiant est à risques et manque un site web. Ajoutez un site web et changez le mot de passe pour une meilleure sécurité." }, "vulnerablePassword": { - "message": "Vulnerable password." + "message": "Mot de passe vulnérable." }, "changeNow": { - "message": "Change now" + "message": "Changer maintenant" }, "missingWebsite": { "message": "Site Web manquant" @@ -5961,7 +6006,7 @@ "message": "Numéro de carte" }, "errorCannotDecrypt": { - "message": "Error: Cannot decrypt" + "message": "Erreur : décryptage impossible" }, "removeMasterPasswordForOrgUserKeyConnector": { "message": "Votre organisation n'utilise plus les mots de passe principaux pour se connecter à Bitwarden. Pour continuer, vérifiez l'organisation et le domaine." @@ -6092,46 +6137,52 @@ } }, "acceptTransfer": { - "message": "Accept transfer" + "message": "Accepter le transfert" }, "declineAndLeave": { - "message": "Decline and leave" + "message": "Refuser et quitter" }, "whyAmISeeingThis": { - "message": "Why am I seeing this?" + "message": "Pourquoi vois-je ceci ?" }, "items": { - "message": "Items" + "message": "Éléments" }, "searchResults": { - "message": "Search results" + "message": "Résultats de la recherche" }, "resizeSideNavigation": { - "message": "Resize side navigation" + "message": "Ajuster la taille de la navigation latérale" }, "whoCanView": { - "message": "Who can view" + "message": "Qui peut visionner" }, "specificPeople": { - "message": "Specific people" + "message": "Personnes spécifiques" }, "emailVerificationDesc": { - "message": "After sharing this Send link, individuals will need to verify their email with a code to view this Send." + "message": "Après avoir partagé ce lien Send, les individus devront vérifier leur courriel à l'aide d'un code afin de voir ce Send." }, "enterMultipleEmailsSeparatedByComma": { - "message": "Enter multiple emails by separating with a comma." + "message": "Entrez plusieurs courriels en les séparant d'une virgule." + }, + "emailsRequiredChangeAccessType": { + "message": "La vérification de courriel requiert au moins une adresse courriel. Pour retirer toutes les adresses, changez le type d'accès ci-dessus." }, "emailPlaceholder": { - "message": "user@bitwarden.com , user@acme.com" + "message": "utilisateur@bitwarden.com , utilisateur@acme.com" }, "downloadBitwardenApps": { - "message": "Download Bitwarden apps" + "message": "Télécharger les applications Bitwarden" }, "emailProtected": { - "message": "Email protected" + "message": "Courriel protégé" }, "sendPasswordHelperText": { - "message": "Individuals will need to enter the password to view this Send", + "message": "Les individus devront entrer le mot de passe pour visionner ce Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "Échec de la vérification d'utilisateur." } } diff --git a/apps/browser/src/_locales/gl/messages.json b/apps/browser/src/_locales/gl/messages.json index e710a489f9a..c84e1f56a95 100644 --- a/apps/browser/src/_locales/gl/messages.json +++ b/apps/browser/src/_locales/gl/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Código de verificación non válido" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ copiado", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration of a list of logins that are at-risk." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Ligazón do Send copiado", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/he/messages.json b/apps/browser/src/_locales/he/messages.json index a76cbb711a9..5462940fa63 100644 --- a/apps/browser/src/_locales/he/messages.json +++ b/apps/browser/src/_locales/he/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "קוד אימות שגוי" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "השדה $VALUE$ הועתק לזיכרון", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "איור של רשימת כניסות בסיכון." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "צור במהירות סיסמה חזקה וייחודית עם תפריט המילוי האוטומטי של Bitwarden באתר שבסיכון.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "קישור סֵנְד הועתק", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/hi/messages.json b/apps/browser/src/_locales/hi/messages.json index d30cbd2cc6e..1fe7310a1f5 100644 --- a/apps/browser/src/_locales/hi/messages.json +++ b/apps/browser/src/_locales/hi/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "सत्यापन कोड अवैध है" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ कॉपी हो गया है।", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "जोखिमग्रस्त लॉगिन की सूची का चित्रण।" }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send link copied", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/hr/messages.json b/apps/browser/src/_locales/hr/messages.json index 98fdee3b657..b73f52ffaf5 100644 --- a/apps/browser/src/_locales/hr/messages.json +++ b/apps/browser/src/_locales/hr/messages.json @@ -29,7 +29,7 @@ "message": "Prijava pristupnim ključem" }, "unlockWithPasskey": { - "message": "Unlock with passkey" + "message": "Otključaj prisutupnim ključem" }, "useSingleSignOn": { "message": "Jedinstvena prijava (SSO)" @@ -574,10 +574,10 @@ "message": "Arhivirane stavke biti će prikazane ovdje i biti će izuzete iz rezultata općih pretraga i preporuka auto-ispune." }, "itemArchiveToast": { - "message": "Item archived" + "message": "Stavka arhivirana" }, "itemUnarchivedToast": { - "message": "Item unarchived" + "message": "Stavka vraćena iz arhive" }, "archiveItem": { "message": "Arhiviraj stavku" @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Nevažeći kôd za provjeru" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": " kopirano", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Ilustracija liste rizičnih prijava." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Brzo generiraj jake, jedinstvene lozinke koristeći Bitwarden dijalog auto-ispune direktno na stranici.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Kopirana poveznica Senda", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/hu/messages.json b/apps/browser/src/_locales/hu/messages.json index e6765219f15..5736378a638 100644 --- a/apps/browser/src/_locales/hu/messages.json +++ b/apps/browser/src/_locales/hu/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Érvénytelen ellenőrző kód" }, + "invalidEmailOrVerificationCode": { + "message": "Az email cím vagy az ellenőrző kód érvénytelen." + }, "valueCopied": { "message": "$VALUE$ másolásra került.", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "A kockázatos bejelentkezések listájának illusztrációja." }, + "welcomeDialogGraphicAlt": { + "message": "A Bitwarden széf oldal elrendezésének illusztrációja." + }, "generatePasswordSlideDesc": { "message": "Gyorsan generálhatunk erős, egyedi jelszót a Bitwarden automatikus kitöltési menüjével a kockázatos webhelyen.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ óra", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Másoljuk és osszuk meg ezt a Send elem hivatkozást. A Send elem bárki számára elérhető lesz, aki rendelkezik a hivatkozással a következő $TIME$ alatt.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Másoljuk és osszuk meg ezt a Send elem hivatkozást. A Send elem bárki számára elérhető lesz, aki rendelkezik a hivatkozással és a jelszóval a következő $TIME$ alatt.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Másoljuk és osszuk meg ezt a Send hivatkozást. Megtekinthetik a megadott személyek a következő $TIME$ intervallumban.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "A Send hivatkozás másolásra került.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Írjunk be több email címet vesszővel elválasztva." }, + "emailsRequiredChangeAccessType": { + "message": "Az email cím ellenőrzéshez legalább egy email cím szükséges. Az összes email cím eltávolításához módosítsuk a fenti hozzáférési típust." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "A személyeknek meg kell adniuk a jelszót a Send elem megtekintéséhez.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "A felhasználó ellenőrzése sikertelen volt." } } diff --git a/apps/browser/src/_locales/id/messages.json b/apps/browser/src/_locales/id/messages.json index ccf35569f36..63e975466ed 100644 --- a/apps/browser/src/_locales/id/messages.json +++ b/apps/browser/src/_locales/id/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Kode verifikasi tidak valid" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ disalin", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Gambaran daftar info masuk yang berpotensi bahaya." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Hasilkan kata sandi yang kuat dan unik dengan cepat dengan menu isi otomatis Bitwarden pada situs yang berpotensi bahaya.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Tautan Send disalin", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/it/messages.json b/apps/browser/src/_locales/it/messages.json index 42efa025207..d66dfe7bfba 100644 --- a/apps/browser/src/_locales/it/messages.json +++ b/apps/browser/src/_locales/it/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Codice di verifica non valido" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ copiata", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustrazione di una lista di login a rischio." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Genera rapidamente una parola d'accesso forte e unica con il menu' di riempimento automatico Bitwarden nel sito a rischio.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Link del Send copiato", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Inserisci più indirizzi email separandoli con virgole." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/ja/messages.json b/apps/browser/src/_locales/ja/messages.json index 915308cec13..ecb03f3321d 100644 --- a/apps/browser/src/_locales/ja/messages.json +++ b/apps/browser/src/_locales/ja/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "認証コードが間違っています" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ をコピーしました", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "危険な状態にあるログイン情報の一覧表示の例" }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Bitwarden の自動入力メニューで、強力で一意なパスワードをすぐに生成しましょう。", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send リンクをコピーしました", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/ka/messages.json b/apps/browser/src/_locales/ka/messages.json index 791664e6eec..17c86de13fb 100644 --- a/apps/browser/src/_locales/ka/messages.json +++ b/apps/browser/src/_locales/ka/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "არასწორი გადამოწმების კოდი" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ copied", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration of a list of logins that are at-risk." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send link copied", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/km/messages.json b/apps/browser/src/_locales/km/messages.json index c28007c3838..51ca51960d7 100644 --- a/apps/browser/src/_locales/km/messages.json +++ b/apps/browser/src/_locales/km/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ copied", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration of a list of logins that are at-risk." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send link copied", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/kn/messages.json b/apps/browser/src/_locales/kn/messages.json index faef7703a66..0dd4699a9a2 100644 --- a/apps/browser/src/_locales/kn/messages.json +++ b/apps/browser/src/_locales/kn/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ ನಕಲಿಸಲಾಗಿದೆ", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration of a list of logins that are at-risk." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send link copied", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/ko/messages.json b/apps/browser/src/_locales/ko/messages.json index b4a04e75e43..66467d99888 100644 --- a/apps/browser/src/_locales/ko/messages.json +++ b/apps/browser/src/_locales/ko/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "유효하지 않은 확인 코드" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$를 클립보드에 복사함", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration of a list of logins that are at-risk." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send 링크 복사됨", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/lt/messages.json b/apps/browser/src/_locales/lt/messages.json index 68eb11aa234..a4cfb34942c 100644 --- a/apps/browser/src/_locales/lt/messages.json +++ b/apps/browser/src/_locales/lt/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Neteisingas patvirtinimo kodas" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "Nukopijuota $VALUE$", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration of a list of logins that are at-risk." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send link copied", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/lv/messages.json b/apps/browser/src/_locales/lv/messages.json index 6eaf545e390..d6e9a171978 100644 --- a/apps/browser/src/_locales/lv/messages.json +++ b/apps/browser/src/_locales/lv/messages.json @@ -574,10 +574,10 @@ "message": "Šeit parādīsies arhivētie vienumi, un tie netiks iekļauti vispārējās meklēšanas iznākumos un automātiskās aizpildes ieteikumos." }, "itemArchiveToast": { - "message": "Item archived" + "message": "Vienums ievietots arhīvā" }, "itemUnarchivedToast": { - "message": "Item unarchived" + "message": "Vienums izņemts no arhīva" }, "archiveItem": { "message": "Arhivēt vienumu" @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Nederīgs apliecinājuma kods" }, + "invalidEmailOrVerificationCode": { + "message": "Nederīga e-pasta adrese vai apliecinājuma kods" + }, "valueCopied": { "message": "$VALUE$ ir starpliktuvē", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Riskam pakļauto pieteikšanās vienumu saraksta attēlojums." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Riskam pakļauto vienumu vietnē ar automātiskās aizpildes izvēlni var ātri izveidot stipru, neatkārtojamu paroli.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send saite ievietota starpliktuvē", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "E-pasta apliecināšanai ir nepieciešama vismaz viena e-pasta adrese. Lai noņemtu visas e-pasta adreses, augstāk jānomaina piekļūšanas veids." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Cilvēkiem būs jāievada parole, lai apskatītu šo Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "Lietotāja apliecināšana neizdevās." } } diff --git a/apps/browser/src/_locales/ml/messages.json b/apps/browser/src/_locales/ml/messages.json index db48220ffbb..61b9f4d45ab 100644 --- a/apps/browser/src/_locales/ml/messages.json +++ b/apps/browser/src/_locales/ml/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ പകർത്തി", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration of a list of logins that are at-risk." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send link copied", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/mr/messages.json b/apps/browser/src/_locales/mr/messages.json index abf2f7db968..2a793784aa2 100644 --- a/apps/browser/src/_locales/mr/messages.json +++ b/apps/browser/src/_locales/mr/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ copied", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration of a list of logins that are at-risk." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send link copied", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/my/messages.json b/apps/browser/src/_locales/my/messages.json index c28007c3838..51ca51960d7 100644 --- a/apps/browser/src/_locales/my/messages.json +++ b/apps/browser/src/_locales/my/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ copied", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration of a list of logins that are at-risk." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send link copied", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/nb/messages.json b/apps/browser/src/_locales/nb/messages.json index 4689cb23b7a..d19dc3571e2 100644 --- a/apps/browser/src/_locales/nb/messages.json +++ b/apps/browser/src/_locales/nb/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Ugyldig bekreftelseskode" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ er kopiert", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration of a list of logins that are at-risk." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send-lenken ble kopiert", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/ne/messages.json b/apps/browser/src/_locales/ne/messages.json index c28007c3838..51ca51960d7 100644 --- a/apps/browser/src/_locales/ne/messages.json +++ b/apps/browser/src/_locales/ne/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ copied", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration of a list of logins that are at-risk." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send link copied", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/nl/messages.json b/apps/browser/src/_locales/nl/messages.json index 044b3cfaa64..0d3ed844a15 100644 --- a/apps/browser/src/_locales/nl/messages.json +++ b/apps/browser/src/_locales/nl/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Ongeldige verificatiecode" }, + "invalidEmailOrVerificationCode": { + "message": "Ongeldig e-mailadres verificatiecode" + }, "valueCopied": { "message": "$VALUE$ gekopieerd", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Voorbeeld van een lijst van risicovolle inloggegevens." }, + "welcomeDialogGraphicAlt": { + "message": "Illustratie van de lay-out van de Bitwarden-kluispagina." + }, "generatePasswordSlideDesc": { "message": "Genereer snel een sterk, uniek wachtwoord met het automatisch invulmenu van Bitwarden op de risicovolle website.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ uur", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Kopieer en deel deze Send-link. De Send is beschikbaar voor iedereen met de link voor de volgende $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Kopieer en deel deze Send-link. De Send is beschikbaar voor iedereen met de link en het ingestelde wachtwoord voor de volgende $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Kopieer en deel deze Send-link. Het kan worden bekeken door de mensen die je hebt opgegeven voor de volgende $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send-link gekopieerd", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Voer meerdere e-mailadressen in door te scheiden met een komma." }, + "emailsRequiredChangeAccessType": { + "message": "E-mailverificatie vereist ten minste één e-mailadres. Om alle e-mailadressen te verwijderen, moet je het toegangstype hierboven wijzigen." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuen moeten het wachtwoord invoeren om deze Send te bekijken", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "Gebruikersverificatie is mislukt." } } diff --git a/apps/browser/src/_locales/nn/messages.json b/apps/browser/src/_locales/nn/messages.json index c28007c3838..51ca51960d7 100644 --- a/apps/browser/src/_locales/nn/messages.json +++ b/apps/browser/src/_locales/nn/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ copied", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration of a list of logins that are at-risk." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send link copied", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/or/messages.json b/apps/browser/src/_locales/or/messages.json index c28007c3838..51ca51960d7 100644 --- a/apps/browser/src/_locales/or/messages.json +++ b/apps/browser/src/_locales/or/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ copied", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration of a list of logins that are at-risk." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send link copied", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/pl/messages.json b/apps/browser/src/_locales/pl/messages.json index 44c7b5fb6dd..c470af8c1dc 100644 --- a/apps/browser/src/_locales/pl/messages.json +++ b/apps/browser/src/_locales/pl/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Kod weryfikacyjny jest nieprawidłowy" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "Skopiowano $VALUE$", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Ilustracja listy danych logowania, które są zagrożone." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Wygeneruj silne i unikalne hasło dla zagrożonej strony internetowej za pomocą autouzupełniania Bitwarden.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Link wysyłki został skopiowany", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/pt_BR/messages.json b/apps/browser/src/_locales/pt_BR/messages.json index 679173205b1..9b0c2483b2a 100644 --- a/apps/browser/src/_locales/pt_BR/messages.json +++ b/apps/browser/src/_locales/pt_BR/messages.json @@ -574,10 +574,10 @@ "message": "Os itens arquivados aparecerão aqui e serão excluídos dos resultados gerais de busca e das sugestões de preenchimento automático." }, "itemArchiveToast": { - "message": "Item archived" + "message": "Item arquivado" }, "itemUnarchivedToast": { - "message": "Item unarchived" + "message": "Item desarquivado" }, "archiveItem": { "message": "Arquivar item" @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Código de verificação inválido" }, + "invalidEmailOrVerificationCode": { + "message": "E-mail ou código de verificação inválido" + }, "valueCopied": { "message": "$VALUE$ copiado(a)", "description": "Value has been copied to the clipboard.", @@ -988,10 +991,10 @@ "message": "Não" }, "noAuth": { - "message": "Anyone with the link" + "message": "Qualquer um com o link" }, "anyOneWithPassword": { - "message": "Anyone with a password set by you" + "message": "Qualquer pessoa com uma senha configurada por você" }, "location": { "message": "Localização" @@ -2052,7 +2055,7 @@ "message": "E-mail" }, "emails": { - "message": "Emails" + "message": "E-mails" }, "phone": { "message": "Telefone" @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Ilustração de uma lista de credenciais em risco." }, + "welcomeDialogGraphicAlt": { + "message": "Ilustração do layout da página do Cofre do Bitwarden." + }, "generatePasswordSlideDesc": { "message": "Gere uma senha forte e única com rapidez com o menu de preenchimento automático no site em risco.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ horas", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copie e compartilhe este link do Send. O Send ficará disponível para qualquer um com o link por $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copie e compartilhe este link do Send. O Send ficará disponível para qualquer um com o link e a senha por $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copie e compartilhe este link do Send. Ele pode ser visto pelas pessoas que você especificou pelos próximos $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Link do Send copiado", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -5961,7 +6006,7 @@ "message": "Número do cartão" }, "errorCannotDecrypt": { - "message": "Error: Cannot decrypt" + "message": "Erro: Não é possível descriptografar" }, "removeMasterPasswordForOrgUserKeyConnector": { "message": "A sua organização não está mais usando senhas principais para se conectar ao Bitwarden. Para continuar, verifique a organização e o domínio." @@ -6101,37 +6146,43 @@ "message": "Por que estou vendo isso?" }, "items": { - "message": "Items" + "message": "Itens" }, "searchResults": { - "message": "Search results" + "message": "Resultados da busca" }, "resizeSideNavigation": { "message": "Redimensionar navegação lateral" }, "whoCanView": { - "message": "Who can view" + "message": "Quem pode visualizar" }, "specificPeople": { - "message": "Specific people" + "message": "Pessoas específicas" }, "emailVerificationDesc": { - "message": "After sharing this Send link, individuals will need to verify their email with a code to view this Send." + "message": "Após compartilhar este link de Send, indivíduos precisarão verificar seus e-mails com um código para visualizar este Send." }, "enterMultipleEmailsSeparatedByComma": { - "message": "Enter multiple emails by separating with a comma." + "message": "Digite vários e-mails, separados com uma vírgula." + }, + "emailsRequiredChangeAccessType": { + "message": "A verificação de e-mail requer pelo menos um endereço de e-mail. Para remover todos, altere o tipo de acesso acima." }, "emailPlaceholder": { - "message": "user@bitwarden.com , user@acme.com" + "message": "usuário@bitwarden.com , usuário@acme.com" }, "downloadBitwardenApps": { - "message": "Download Bitwarden apps" + "message": "Baixar aplicativos do Bitwarden" }, "emailProtected": { - "message": "Email protected" + "message": "Protegido por e-mail" }, "sendPasswordHelperText": { - "message": "Individuals will need to enter the password to view this Send", + "message": "Os indivíduos precisarão digitar a senha para ver este Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "Falha na verificação do usuário." } } diff --git a/apps/browser/src/_locales/pt_PT/messages.json b/apps/browser/src/_locales/pt_PT/messages.json index 9094e04094d..5d498908fa5 100644 --- a/apps/browser/src/_locales/pt_PT/messages.json +++ b/apps/browser/src/_locales/pt_PT/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Código de verificação inválido" }, + "invalidEmailOrVerificationCode": { + "message": "E-mail ou código de verificação inválido" + }, "valueCopied": { "message": "$VALUE$ copiado(a)", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Ilustração de uma lista de credenciais que estão em risco." }, + "welcomeDialogGraphicAlt": { + "message": "Ilustração do layout da página do cofre Bitwarden." + }, "generatePasswordSlideDesc": { "message": "Gira rapidamente uma palavra-passe forte e única com o menu de preenchimento automático do Bitwarden no site em risco.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ horas", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copie e partilhe este link do Send. O Send estará disponível para qualquer pessoa com o link durante os próximos $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copie e partilhe este link do Send. O Send estará disponível para qualquer pessoa com o link e palavras-passe durante os próximos $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copie e partilhe este link do Send. Pode ser visualizado pelas pessoas que especificou durante os próximos $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Link do Send copiado", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Introduza vários e-mails, separados por vírgula." }, + "emailsRequiredChangeAccessType": { + "message": "A verificação por e-mail requer pelo menos um endereço de e-mail. Para remover todos os e-mails, altere o tipo de acesso acima." + }, "emailPlaceholder": { "message": "utilizador@bitwarden.com , utilizador@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Os indivíduos terão de introduzir a palavra-passe para ver este Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "Falha na verificação do utilizador." } } diff --git a/apps/browser/src/_locales/ro/messages.json b/apps/browser/src/_locales/ro/messages.json index 47f7ae9cae3..eb6c7eb2a66 100644 --- a/apps/browser/src/_locales/ro/messages.json +++ b/apps/browser/src/_locales/ro/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Cod de verificare nevalid" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": " $VALUE$ s-a copiat", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration of a list of logins that are at-risk." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send link copied", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/ru/messages.json b/apps/browser/src/_locales/ru/messages.json index d1fb3de89a6..75a2afb4e1a 100644 --- a/apps/browser/src/_locales/ru/messages.json +++ b/apps/browser/src/_locales/ru/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Неверный код подтверждения" }, + "invalidEmailOrVerificationCode": { + "message": "Неверный email или код подтверждения" + }, "valueCopied": { "message": "$VALUE$ скопировано", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Иллюстрация списка логинов, которые подвержены риску." }, + "welcomeDialogGraphicAlt": { + "message": "Иллюстрация макета страницы хранилища Bitwarden." + }, "generatePasswordSlideDesc": { "message": "Быстро сгенерируйте надежный уникальный пароль с помощью меню автозаполнения Bitwarden на сайте, подверженном риску.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ час.", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Скопируйте и поделитесь этой ссылкой Send. Send будет доступна всем, у кого есть ссылка, в течение следующих $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Скопируйте и поделитесь этой ссылкой Send. Send будет доступна всем, у кого есть ссылка и пароль в течение следующих $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Скопируйте и распространите эту ссылку для Send. Она может быть просмотрена указанными вами пользователями в течение $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Ссылка на Send скопирована", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Введите несколько email, разделяя их запятой." }, + "emailsRequiredChangeAccessType": { + "message": "Для проверки электронной почты требуется как минимум один адрес email. Чтобы удалить все адреса электронной почты, измените тип доступа выше." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Пользователям необходимо будет ввести пароль для просмотра этой Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "Проверка пользователя не удалась." } } diff --git a/apps/browser/src/_locales/si/messages.json b/apps/browser/src/_locales/si/messages.json index e70c620eaf8..d7e63d70f87 100644 --- a/apps/browser/src/_locales/si/messages.json +++ b/apps/browser/src/_locales/si/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "වලංගු නොවන සත්යාපන කේතය" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ පිටපත් කරන ලදි", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration of a list of logins that are at-risk." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send link copied", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/sk/messages.json b/apps/browser/src/_locales/sk/messages.json index e1886098a31..806e83e0b1f 100644 --- a/apps/browser/src/_locales/sk/messages.json +++ b/apps/browser/src/_locales/sk/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Neplatný verifikačný kód" }, + "invalidEmailOrVerificationCode": { + "message": "Neplatný e-mailový alebo overovací kód" + }, "valueCopied": { "message": " skopírované", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Príklady zoznamu prihlásení, ktoré sú ohrozené." }, + "welcomeDialogGraphicAlt": { + "message": "Ilustrácia rozloženia stránky trezoru Bitwarden." + }, "generatePasswordSlideDesc": { "message": "Rýchlo generujte silné, jedinečné heslo pomocu ponuky automatického vypĺňania Bitwardenu na ohrozených stránkach.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hod.", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Skopírujte a zdieľajte tento odkaz na Send. Send bude dostupný každému, kto má odkaz, počas nasledujúcich $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Skopírujte a zdieľajte tento odkaz na Send. Send bude dostupný každému, kto má odkaz a heslo od vás, počas nasledujúcich $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Skopírujte a zdieľajte tento odkaz na Send. Zobraziť ho môžu ľudia, ktorých ste vybrali, počas nasledujúcich $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Skopírovaný odkaz na Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Zadajte viacero e-mailových adries oddelených čiarkou." }, + "emailsRequiredChangeAccessType": { + "message": "Overenie e-mailu vyžaduje aspoň jednu e-mailovú adresu. Ak chcete odstrániť všetky e-maily, zmeňte typ prístupu vyššie." + }, "emailPlaceholder": { "message": "pouzivate@bitwarden.com, pouzivatel@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Jednotlivci budú musieť zadať heslo, aby mohli zobraziť tento Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "Overenie používateľa zlyhalo." } } diff --git a/apps/browser/src/_locales/sl/messages.json b/apps/browser/src/_locales/sl/messages.json index 100a04a3012..88f54663ccd 100644 --- a/apps/browser/src/_locales/sl/messages.json +++ b/apps/browser/src/_locales/sl/messages.json @@ -6,30 +6,30 @@ "message": "Bitwarden logo" }, "extName": { - "message": "Bitwarden Password Manager", + "message": "Bitwarden – Upravitelj gesel", "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": "Doma, na delu ali na poti – Bitwarden enostavno zaščiti vsa vaša gesla, ključe za dostop in občutljive podatke", "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Prijavite se ali ustvarite nov račun za dostop do svojega varnega trezorja." }, "inviteAccepted": { - "message": "Invitation accepted" + "message": "Povabilo sprejeto" }, "createAccount": { "message": "Ustvari račun" }, "newToBitwarden": { - "message": "New to Bitwarden?" + "message": "Novi na Bitwardenu?" }, "logInWithPasskey": { - "message": "Log in with passkey" + "message": "Prijava s ključem za dostop" }, "unlockWithPasskey": { - "message": "Unlock with passkey" + "message": "Odkleni s ključem za dostop" }, "useSingleSignOn": { "message": "Use single sign-on" @@ -38,10 +38,10 @@ "message": "Your organization requires single sign-on." }, "welcomeBack": { - "message": "Welcome back" + "message": "Dobrodošli nazaj" }, "setAStrongPassword": { - "message": "Set a strong password" + "message": "Nastavite močno geslo" }, "finishCreatingYourAccountBySettingAPassword": { "message": "Finish creating your account by setting a password" @@ -90,7 +90,7 @@ "message": "Namig za glavno geslo (neobvezno)" }, "passwordStrengthScore": { - "message": "Password strength score $SCORE$", + "message": "Ocena moči gesla: $SCORE$", "placeholders": { "score": { "content": "$1", @@ -99,7 +99,7 @@ } }, "joinOrganization": { - "message": "Join organization" + "message": "Pridružite se organizaciji" }, "joinOrganizationName": { "message": "Join $ORGANIZATIONNAME$", @@ -156,31 +156,31 @@ "message": "Kopiraj varnostno kodo" }, "copyName": { - "message": "Copy name" + "message": "Kopiraj ime" }, "copyCompany": { - "message": "Copy company" + "message": "Kopiraj podjetje" }, "copySSN": { "message": "Copy Social Security number" }, "copyPassportNumber": { - "message": "Copy passport number" + "message": "Kopiraj številko potnega lista" }, "copyLicenseNumber": { "message": "Copy license number" }, "copyPrivateKey": { - "message": "Copy private key" + "message": "Kopiraj zasebni ključ" }, "copyPublicKey": { - "message": "Copy public key" + "message": "Kopiraj javni ključ" }, "copyFingerprint": { "message": "Copy fingerprint" }, "copyCustomField": { - "message": "Copy $FIELD$", + "message": "Kopiraj $FIELD$", "placeholders": { "field": { "content": "$1", @@ -189,17 +189,17 @@ } }, "copyWebsite": { - "message": "Copy website" + "message": "Kopiraj spletno stran" }, "copyNotes": { - "message": "Copy notes" + "message": "Kopiraj zapiske" }, "copy": { - "message": "Copy", + "message": "Kopiraj", "description": "Copy to clipboard" }, "fill": { - "message": "Fill", + "message": "Izpolni", "description": "This string is used on the vault page to indicate autofilling. Horizontal space is limited in the interface here so try and keep translations as concise as possible." }, "autoFill": { @@ -215,10 +215,10 @@ "message": "Samodejno izpolni identiteto" }, "fillVerificationCode": { - "message": "Fill verification code" + "message": "Izpolni kodo za preverjanje" }, "fillVerificationCodeAria": { - "message": "Fill Verification Code", + "message": "Izpolni kodo za preverjanje", "description": "Aria label for the heading displayed the inline menu for totp code autofill" }, "generatePasswordCopied": { @@ -297,13 +297,13 @@ "message": "Spremeni glavno geslo" }, "continueToWebApp": { - "message": "Continue to web app?" + "message": "Nadaljuj v spletno aplikacijo?" }, "continueToWebAppDesc": { - "message": "Explore more features of your Bitwarden account on the web app." + "message": "Raziščite več funkcij vašega Bitwarden računa na spletni aplikaciji." }, "continueToHelpCenter": { - "message": "Continue to Help Center?" + "message": "Nadaljuj na center za pomoč?" }, "continueToHelpCenterDesc": { "message": "Learn more about how to use Bitwarden on the Help Center." @@ -315,7 +315,7 @@ "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": "Vaše glavno geslo lahko zamenjate v Bitwarden spletni aplikaciji." }, "fingerprintPhrase": { "message": "Identifikacijsko geslo", @@ -332,19 +332,19 @@ "message": "Odjava" }, "aboutBitwarden": { - "message": "About Bitwarden" + "message": "O Bitwardenu" }, "about": { "message": "O programu" }, "moreFromBitwarden": { - "message": "More from Bitwarden" + "message": "Več od Bitwardena" }, "continueToBitwardenDotCom": { - "message": "Continue to bitwarden.com?" + "message": "Nadaljuj na bitwarden.com?" }, "bitwardenForBusiness": { - "message": "Bitwarden for Business" + "message": "Bitwarden za podjetja" }, "bitwardenAuthenticator": { "message": "Bitwarden Authenticator" @@ -398,10 +398,10 @@ } }, "newFolder": { - "message": "New folder" + "message": "Nova mapa" }, "folderName": { - "message": "Folder name" + "message": "Ime mape" }, "folderHintText": { "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" @@ -440,7 +440,7 @@ "message": "Sinhronizacija" }, "syncNow": { - "message": "Sync now" + "message": "Sinhroniziraj zdaj" }, "lastSync": { "message": "Zadnja sinhronizacija:" @@ -456,7 +456,7 @@ "message": "Avtomatično generiraj močna, edinstvena gesla za vaše prijave." }, "bitWebVaultApp": { - "message": "Bitwarden web app" + "message": "Bitwarden spletna aplikacija" }, "select": { "message": "Izberi" @@ -468,7 +468,7 @@ "message": "Generate passphrase" }, "passwordGenerated": { - "message": "Password generated" + "message": "Geslo generirano" }, "passphraseGenerated": { "message": "Passphrase generated" @@ -493,7 +493,7 @@ "description": "Card header for password generator include block" }, "uppercaseDescription": { - "message": "Include uppercase characters", + "message": "Vključi velike črke", "description": "Tooltip for the password generator uppercase character checkbox" }, "uppercaseLabel": { @@ -501,7 +501,7 @@ "description": "Label for the password generator uppercase character checkbox" }, "lowercaseDescription": { - "message": "Include lowercase characters", + "message": "Vključi male črke", "description": "Full description for the password generator lowercase character checkbox" }, "lowercaseLabel": { @@ -509,7 +509,7 @@ "description": "Label for the password generator lowercase character checkbox" }, "numbersDescription": { - "message": "Include numbers", + "message": "Vključi števila", "description": "Full description for the password generator numbers checkbox" }, "numbersLabel": { @@ -517,7 +517,7 @@ "description": "Label for the password generator numbers checkbox" }, "specialCharactersDescription": { - "message": "Include special characters", + "message": "Vključi posebne znake", "description": "Full description for the password generator special characters checkbox" }, "numWords": { @@ -540,7 +540,7 @@ "message": "Minimalno posebnih znakov" }, "avoidAmbiguous": { - "message": "Avoid ambiguous characters", + "message": "Izogibaj se dvoumnim znakom", "description": "Label for the avoid ambiguous characters checkbox." }, "generatorPolicyInEffect": { @@ -554,21 +554,21 @@ "message": "Reset search" }, "archiveNoun": { - "message": "Archive", + "message": "Arhiv", "description": "Noun" }, "archiveVerb": { - "message": "Archive", + "message": "Arhiviraj", "description": "Verb" }, "unArchive": { - "message": "Unarchive" + "message": "Odstrani iz arhiva" }, "itemsInArchive": { - "message": "Items in archive" + "message": "Elementi v arhivu" }, "noItemsInArchive": { - "message": "No items in archive" + "message": "Ni elementov v arhivu" }, "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." @@ -592,10 +592,10 @@ "message": "Unarchive and save" }, "upgradeToUseArchive": { - "message": "A premium membership is required to use Archive." + "message": "Za uporabo Arhiva je potrebno premium članstvo." }, "itemRestored": { - "message": "Item has been restored" + "message": "Vnos je bil obnovljen" }, "edit": { "message": "Uredi" @@ -604,13 +604,13 @@ "message": "Pogled" }, "viewAll": { - "message": "View all" + "message": "Poglej vse" }, "showAll": { - "message": "Show all" + "message": "Prikaži vse" }, "viewLess": { - "message": "View less" + "message": "Poglej manj" }, "viewLogin": { "message": "View login" @@ -640,10 +640,10 @@ "message": "Unfavorite" }, "itemAddedToFavorites": { - "message": "Item added to favorites" + "message": "Element dodan med priljubljene" }, "itemRemovedFromFavorites": { - "message": "Item removed from favorites" + "message": "Element odstranen iz priljubljenih" }, "notes": { "message": "Opombe" @@ -709,7 +709,7 @@ "message": "Vault timeout" }, "otherOptions": { - "message": "Other options" + "message": "Ostale možnosti" }, "rateExtension": { "message": "Ocenite to razširitev" @@ -718,25 +718,25 @@ "message": "Vaš brskalnik ne podpira enostavnega kopiranja na odložišče. Prosimo, kopirajte ročno." }, "verifyYourIdentity": { - "message": "Verify your identity" + "message": "Potrdite vašo identiteto" }, "weDontRecognizeThisDevice": { - "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + "message": "Te naprave ne prepoznamo. Vnesite kodo, ki je bila poslana na vaš e-poštni naslov, da potrdite vašo identiteto." }, "continueLoggingIn": { - "message": "Continue logging in" + "message": "Nadaljujte s prijavo" }, "yourVaultIsLocked": { "message": "Vaš trezor je zaklenjen. Za nadaljevanje potrdite svojo identiteto." }, "yourVaultIsLockedV2": { - "message": "Your vault is locked" + "message": "Vaš trezor je zaklenjen" }, "yourAccountIsLocked": { - "message": "Your account is locked" + "message": "Vaš račun je zaklenjen" }, "or": { - "message": "or" + "message": "ali" }, "unlock": { "message": "Odkleni" @@ -758,7 +758,7 @@ "message": "Napačno glavno geslo" }, "invalidMasterPasswordConfirmEmailAndHost": { - "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "message": "Neveljavno glavno geslo. Preverite, da je vaš e-poštni naslov pravilen, ter da je bil vaš račun ustvarjen na $HOST$.", "placeholders": { "host": { "content": "$1", @@ -776,7 +776,7 @@ "message": "Zakleni zdaj" }, "lockAll": { - "message": "Lock all" + "message": "Zakleni vse" }, "immediately": { "message": "Takoj" @@ -833,13 +833,13 @@ "message": "Confirm master password" }, "masterPassword": { - "message": "Master password" + "message": "Glavno geslo" }, "masterPassImportant": { - "message": "Your master password cannot be recovered if you forget it!" + "message": "Če pozabite glavno geslo, ga ne bo mogoče obnoviti!" }, "masterPassHintLabel": { - "message": "Master password hint" + "message": "Namig za glavno geslo" }, "errorOccurred": { "message": "Prišlo je do napake" @@ -876,7 +876,7 @@ "message": "Your new account has been created!" }, "youHaveBeenLoggedIn": { - "message": "You have been logged in!" + "message": "Prijavljeni ste!" }, "youSuccessfullyLoggedIn": { "message": "You successfully logged in" @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Neveljavna koda za preverjanje" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ kopirana", "description": "Value has been copied to the clipboard.", @@ -970,7 +973,7 @@ "message": "Restart registration" }, "expiredLink": { - "message": "Expired link" + "message": "Pretečena povezava" }, "pleaseRestartRegistrationOrTryLoggingIn": { "message": "Please restart registration or try logging in." @@ -994,7 +997,7 @@ "message": "Anyone with a password set by you" }, "location": { - "message": "Location" + "message": "Lokacija" }, "unexpectedError": { "message": "Prišlo je do nepričakovane napake." @@ -1009,10 +1012,10 @@ "message": "Avtentikacija v dveh korakih dodatno varuje vaš račun, saj zahteva, da vsakokratno prijavo potrdite z drugo napravo, kot je varnostni ključ, aplikacija za preverjanje pristnosti, SMS, telefonski klic ali e-pošta. Avtentikacijo v dveh korakih lahko omogočite v spletnem trezorju bitwarden.com. Ali želite spletno stran obiskati sedaj?" }, "twoStepLoginConfirmationContent": { - "message": "Make your account more secure by setting up two-step login in the Bitwarden web app." + "message": "Zavarujte vaš račun tako, da nastavite dvostopenjsko prijavo v Bitwarden spletni aplikaciji." }, "twoStepLoginConfirmationTitle": { - "message": "Continue to web app?" + "message": "Nadaljuj v spletno aplikacijo?" }, "editedFolder": { "message": "Mapa shranjena" @@ -1055,7 +1058,7 @@ "message": "Nov URI" }, "addDomain": { - "message": "Add domain", + "message": "Dodaj domeno", "description": "'Domain' here refers to an internet domain name (e.g. 'bitwarden.com') and the message in whole described the act of putting a domain value into the context." }, "addedItem": { @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration of a list of logins that are at-risk." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send link copied", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6062,7 +6107,7 @@ "message": "Contact your admin to regain access." }, "leaveConfirmationDialogConfirmButton": { - "message": "Leave $ORGANIZATION$", + "message": "Zapusti $ORGANIZATION$", "placeholders": { "organization": { "content": "$1", @@ -6071,10 +6116,10 @@ } }, "howToManageMyVault": { - "message": "How do I manage my vault?" + "message": "Kako uporabljam svoj trezor?" }, "transferItemsToOrganizationTitle": { - "message": "Transfer items to $ORGANIZATION$", + "message": "Prenesi elemente v $ORGANIZATION$", "placeholders": { "organization": { "content": "$1", @@ -6092,19 +6137,19 @@ } }, "acceptTransfer": { - "message": "Accept transfer" + "message": "Sprejmi prenos" }, "declineAndLeave": { "message": "Decline and leave" }, "whyAmISeeingThis": { - "message": "Why am I seeing this?" + "message": "Zakaj se mi to prikazuje?" }, "items": { - "message": "Items" + "message": "Elementi" }, "searchResults": { - "message": "Search results" + "message": "Rezultati iskanja" }, "resizeSideNavigation": { "message": "Resize side navigation" @@ -6121,11 +6166,14 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { - "message": "user@bitwarden.com , user@acme.com" + "message": "uporabnik@bitwarden.com , uporabnik@acme.com" }, "downloadBitwardenApps": { - "message": "Download Bitwarden apps" + "message": "Prenesi Bitwarden aplikacije" }, "emailProtected": { "message": "Email protected" @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/sr/messages.json b/apps/browser/src/_locales/sr/messages.json index e91e003c8e0..b375ca5d536 100644 --- a/apps/browser/src/_locales/sr/messages.json +++ b/apps/browser/src/_locales/sr/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Неисправан верификациони код" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ копиран(а/о)", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Илустрација листе пријаве које су ризичне." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Брзо генеришите снажну, јединствену лозинку са Bitwarden менијем аутопуњења за коришћење на ризичном сајту.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send линк је копиран", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/sv/messages.json b/apps/browser/src/_locales/sv/messages.json index 484817b0210..eb5a7fef645 100644 --- a/apps/browser/src/_locales/sv/messages.json +++ b/apps/browser/src/_locales/sv/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Ogiltig verifieringskod" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ har kopierats", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration av en lista över inloggningar som är i riskzonen." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Skapa snabbt ett starkt, unikt lösenord med Bitwardens autofyllmeny på riskwebbplatsen.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ timmar", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Kopiera och dela denna Send-länk. Denna Send kommer att vara tillgänglig för alla med länken för nästa $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Kopiera och dela denna Send-länk. Denna Send kommer att vara tillgänglig för alla med den länk och lösenord du angav för nästa $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Kopiera och dela denna Send-länk. Den kan visas av personer som du har angivet nästa $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Skicka länk kopierad", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Ange flera e-postadresser genom att separera dem med kommatecken." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "användare@bitwarden.com , användare@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individer måste ange lösenordet för att visa denna Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/ta/messages.json b/apps/browser/src/_locales/ta/messages.json index 3e76c0ab0d1..72ad809e723 100644 --- a/apps/browser/src/_locales/ta/messages.json +++ b/apps/browser/src/_locales/ta/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "தவறான சரிபார்ப்புக் குறியீடு" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ நகலெடுக்கப்பட்டது", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "ஆபத்தில் உள்ள உள்நுழைவுகளின் பட்டியலின் விளக்கம்." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "ஆபத்தில் உள்ள தளத்தில் உள்ள Bitwarden தானாக நிரப்பு மெனுவுடன் ஒரு வலுவான, தனிப்பட்ட கடவுச்சொல்லை விரைவாக உருவாக்குங்கள்.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "அனுப்பு இணைப்பு நகலெடுக்கப்பட்டது", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/te/messages.json b/apps/browser/src/_locales/te/messages.json index c28007c3838..51ca51960d7 100644 --- a/apps/browser/src/_locales/te/messages.json +++ b/apps/browser/src/_locales/te/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ copied", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration of a list of logins that are at-risk." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send link copied", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/th/messages.json b/apps/browser/src/_locales/th/messages.json index 5ec728189a8..82878eb3b52 100644 --- a/apps/browser/src/_locales/th/messages.json +++ b/apps/browser/src/_locales/th/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "รหัสยืนยันไม่ถูกต้อง" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "คัดลอก $VALUE$ แล้ว", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "ภาพประกอบรายการข้อมูลเข้าสู่ระบบที่มีความเสี่ยง" }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "สร้างรหัสผ่านที่รัดกุมและไม่ซ้ำกันอย่างรวดเร็วด้วยเมนูป้อนอัตโนมัติของ Bitwarden บนเว็บไซต์ที่มีความเสี่ยง", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "คัดลอกลิงก์ Send แล้ว", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/tr/messages.json b/apps/browser/src/_locales/tr/messages.json index 7d5b31a9aba..b3d1d46c9a5 100644 --- a/apps/browser/src/_locales/tr/messages.json +++ b/apps/browser/src/_locales/tr/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Geçersiz doğrulama kodu" }, + "invalidEmailOrVerificationCode": { + "message": "E-posta veya doğrulama kodu geçersiz" + }, "valueCopied": { "message": "$VALUE$ kopyalandı", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Risk altındaki hesap listesinin illüstrasyonu." }, + "welcomeDialogGraphicAlt": { + "message": "Bitwarden kasa sayfası düzeninin illüstrasyonu." + }, "generatePasswordSlideDesc": { "message": "Riskli sitede Bitwarden otomatik doldurma menüsünü kullanarak hızlıca güçlü ve benzersiz bir parola oluştur.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ saat", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Bu Send bağlantısını kopyalayıp paylaşın. Bu Send'e önümüzdeki $TIME$ boyunca bağlantıya sahip herkes ulaşabilecektir.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Bu Send bağlantısını kopyalayıp paylaşın. Bu Send'e önümüzdeki $TIME$ boyunca bağlantıya ve parolaya sahip herkes ulaşabilecektir.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Bu Send bağlantısını kopyalayıp paylaşın. Belirlediğiniz kişiler bağlantıyı önümüzdeki $TIME$ boyunca kullanabilir.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send bağlantısı kopyalandı", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "E-posta adreslerini virgülle ayırarak yazın." }, + "emailsRequiredChangeAccessType": { + "message": "E-posta doğrulaması için en az bir e-posta adresi gerekir. Tüm e-postaları silmek için yukarıdan erişim türünü değiştirin." + }, "emailPlaceholder": { "message": "kullanici@bitwarden.com , kullanici@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Bu Send'i görmek isteyen kişilerin parola girmesi gerekecektir", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "Kullanıcı doğrulaması başarısız oldu." } } diff --git a/apps/browser/src/_locales/uk/messages.json b/apps/browser/src/_locales/uk/messages.json index 49a0c9de25b..541a60b8ff8 100644 --- a/apps/browser/src/_locales/uk/messages.json +++ b/apps/browser/src/_locales/uk/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Недійсний код підтвердження" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ скопійовано", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Ілюстрація списку ризикованих записів." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Швидко згенеруйте надійний, унікальний пароль через меню автозаповнення Bitwarden на сайті з ризикованим паролем.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Посилання на відправлення скопійовано", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Введіть декілька адрес е-пошти, розділяючи їх комою." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Особам необхідно ввести пароль для перегляду цього відправлення", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/vi/messages.json b/apps/browser/src/_locales/vi/messages.json index 4f1165835cc..ebd3a2500aa 100644 --- a/apps/browser/src/_locales/vi/messages.json +++ b/apps/browser/src/_locales/vi/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Mã xác minh không đúng" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "Đã sao chép $VALUE$", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Minh họa danh sách các tài khoản đăng nhập có rủi ro." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Tạo nhanh một mật khẩu mạnh, duy nhất bằng menu tự động điền của Bitwarden trên trang web có nguy cơ.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Đã sao chép liên kết Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/zh_CN/messages.json b/apps/browser/src/_locales/zh_CN/messages.json index c9dd30ab08e..5fc0b632676 100644 --- a/apps/browser/src/_locales/zh_CN/messages.json +++ b/apps/browser/src/_locales/zh_CN/messages.json @@ -574,10 +574,10 @@ "message": "已归档的项目将显示在此处,并将被排除在一般搜索结果和自动填充建议之外。" }, "itemArchiveToast": { - "message": "Item archived" + "message": "项目已归档" }, "itemUnarchivedToast": { - "message": "Item unarchived" + "message": "项目已取消归档" }, "archiveItem": { "message": "归档项目" @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "无效的验证码" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ 已复制", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "存在风险的登录列表示意图。" }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "在存在风险的网站上,使用 Bitwarden 自动填充菜单快速生成强大且唯一的密码。", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ 小时", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "复制并分享此 Send 链接。在接下来的 $TIME$ 内,拥有此链接的任何人都可以访问此 Send。", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "复制并分享此 Send 链接。在接下来的 $TIME$ 内,拥有此链接以及您设置的密码的任何人都可以访问此 Send。", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "复制并分享此 Send 链接。在接下来的 $TIME$ 内,您指定的人员可查看此 Send。", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send 链接已复制", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "输入多个电子邮箱(使用逗号分隔)。" }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com, user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "个人需要输入密码才能查看此 Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/zh_TW/messages.json b/apps/browser/src/_locales/zh_TW/messages.json index 8da1b2ad08f..d23106948ad 100644 --- a/apps/browser/src/_locales/zh_TW/messages.json +++ b/apps/browser/src/_locales/zh_TW/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "驗證碼無效" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ 已複製", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "有風險登入清單的示意圖。" }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "在有風險的網站上,透過 Bitwarden 自動填入選單快速產生強且唯一的密碼。", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ 小時", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "複製並分享此 Send 連結。任何擁有此連結的人,都可在接下來的 $TIME$ 內存取該 Send。", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "複製並分享此 Send 連結。任何擁有此連結與您所設定密碼的人,都可在接下來的 $TIME$ 內存取該 Send。", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "複製並分享此 Send 連結。在接下來的 $TIME$ 內,只有您指定的人可以檢視。", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "已複製 Send 連結", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "請以逗號分隔輸入多個電子郵件地址。" }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "對方必須輸入密碼才能檢視此 Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/store/locales/sl/copy.resx b/apps/browser/store/locales/sl/copy.resx index b2a95ed5689..07864dcc1f1 100644 --- a/apps/browser/store/locales/sl/copy.resx +++ b/apps/browser/store/locales/sl/copy.resx @@ -118,10 +118,10 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden Password Manager + Bitwarden – Upravitelj gesel - At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. + Doma, na delu ali na poti – Bitwarden na enostaven način zaščiti vsa vaša gesla, ključe za dostop in občutljive podatke. Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! @@ -169,7 +169,7 @@ End-to-end encrypted credential management solutions from Bitwarden empower orga - At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. + Doma, na delu ali na poti – Bitwarden na enostaven način zaščiti vsa vaša gesla, ključe za dostop in občutljive podatke. Sinhronizirajte svoj trezor gesel in dostopajte do njega z več naprav From 40c8139e1c07740f5e23c78d55a14a80021cb167 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Fri, 20 Feb 2026 13:45:30 +0100 Subject: [PATCH 40/89] Update sdk to 550 (#19084) --- package-lock.json | 16 ++++++++-------- package.json | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index fb1111a82b9..9f6e82d98ef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,8 +23,8 @@ "@angular/platform-browser": "20.3.16", "@angular/platform-browser-dynamic": "20.3.16", "@angular/router": "20.3.16", - "@bitwarden/commercial-sdk-internal": "0.2.0-main.546", - "@bitwarden/sdk-internal": "0.2.0-main.546", + "@bitwarden/commercial-sdk-internal": "0.2.0-main.550", + "@bitwarden/sdk-internal": "0.2.0-main.550", "@electron/fuses": "1.8.0", "@emotion/css": "11.13.5", "@koa/multer": "4.0.0", @@ -4936,9 +4936,9 @@ "link": true }, "node_modules/@bitwarden/commercial-sdk-internal": { - "version": "0.2.0-main.546", - "resolved": "https://registry.npmjs.org/@bitwarden/commercial-sdk-internal/-/commercial-sdk-internal-0.2.0-main.546.tgz", - "integrity": "sha512-3lIQSb1yYSpDqhgT2uqHjPC88yVL7rWR08i0XD0BQJMFfN0FcB378r2Fq6d5TMXLPEYZ8PR62BCDB+tYKM7FPw==", + "version": "0.2.0-main.550", + "resolved": "https://registry.npmjs.org/@bitwarden/commercial-sdk-internal/-/commercial-sdk-internal-0.2.0-main.550.tgz", + "integrity": "sha512-hYdGV3qs+kKrAMTIvMfolWz23XXZ8bJGzMGi+gh5EBpjTE4OsAsLKp0JDgpjlpE+cdheSFXyhTU9D1Ujdqzzrg==", "license": "BITWARDEN SOFTWARE DEVELOPMENT KIT LICENSE AGREEMENT", "dependencies": { "type-fest": "^4.41.0" @@ -5041,9 +5041,9 @@ "link": true }, "node_modules/@bitwarden/sdk-internal": { - "version": "0.2.0-main.546", - "resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.546.tgz", - "integrity": "sha512-KGPyP1pr7aIBaJ9Knibpfjydo/27Rlve77X4ENmDIwrSJ9FB3o2B6D3UXpNNVyXKt2Ii1C+rNT7ezMRO25Qs4A==", + "version": "0.2.0-main.550", + "resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.550.tgz", + "integrity": "sha512-uAGgP+Y2FkxOZ74+9C4JHaM+YbJTI3806akeDg7w2yvfNNryIsLncwvb8FoFgiN6dEY1o9YSzuuv0YYUnbAMww==", "license": "GPL-3.0", "dependencies": { "type-fest": "^4.41.0" diff --git a/package.json b/package.json index c18112989fe..1795e93cf83 100644 --- a/package.json +++ b/package.json @@ -161,8 +161,8 @@ "@angular/platform-browser": "20.3.16", "@angular/platform-browser-dynamic": "20.3.16", "@angular/router": "20.3.16", - "@bitwarden/commercial-sdk-internal": "0.2.0-main.546", - "@bitwarden/sdk-internal": "0.2.0-main.546", + "@bitwarden/commercial-sdk-internal": "0.2.0-main.550", + "@bitwarden/sdk-internal": "0.2.0-main.550", "@electron/fuses": "1.8.0", "@emotion/css": "11.13.5", "@koa/multer": "4.0.0", From bb110122a566ff35fb63ba505ccec6bc14a00f9b Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Fri, 20 Feb 2026 15:28:24 +0100 Subject: [PATCH 41/89] [PM-30144] Implement client-side user-key-rotation-service (#18285) * Implement client-side user-key-rotation-service * Feature flag * Add tests * Fix flag name * Fix build * Prettier * Small clean-up * Codeowners order cleanup * Fix eslint issue * Update sdk to 550 * Cleanup & fix incompatibilities * Prettier --- .github/CODEOWNERS | 1 + .../user-key-rotation.service.spec.ts | 12 +- .../key-rotation/user-key-rotation.service.ts | 24 ++ jest.config.js | 1 + libs/common/src/enums/feature-flag.enum.ts | 2 + libs/user-crypto-management/README.md | 5 + libs/user-crypto-management/eslint.config.mjs | 3 + libs/user-crypto-management/jest.config.js | 11 + libs/user-crypto-management/package.json | 11 + libs/user-crypto-management/project.json | 34 ++ libs/user-crypto-management/src/index.ts | 3 + .../src/user-crypto-management.module.ts | 25 ++ .../user-key-rotation.service.abstraction.ts | 41 +++ .../src/user-key-rotation.service.spec.ts | 295 ++++++++++++++++++ .../src/user-key-rotation.service.ts | 164 ++++++++++ libs/user-crypto-management/test.setup.ts | 1 + .../tsconfig.eslint.json | 6 + libs/user-crypto-management/tsconfig.json | 5 + libs/user-crypto-management/tsconfig.lib.json | 10 + .../user-crypto-management/tsconfig.spec.json | 10 + package-lock.json | 9 + tsconfig.base.json | 2 + 22 files changed, 674 insertions(+), 1 deletion(-) create mode 100644 libs/user-crypto-management/README.md create mode 100644 libs/user-crypto-management/eslint.config.mjs create mode 100644 libs/user-crypto-management/jest.config.js create mode 100644 libs/user-crypto-management/package.json create mode 100644 libs/user-crypto-management/project.json create mode 100644 libs/user-crypto-management/src/index.ts create mode 100644 libs/user-crypto-management/src/user-crypto-management.module.ts create mode 100644 libs/user-crypto-management/src/user-key-rotation.service.abstraction.ts create mode 100644 libs/user-crypto-management/src/user-key-rotation.service.spec.ts create mode 100644 libs/user-crypto-management/src/user-key-rotation.service.ts create mode 100644 libs/user-crypto-management/test.setup.ts create mode 100644 libs/user-crypto-management/tsconfig.eslint.json create mode 100644 libs/user-crypto-management/tsconfig.json create mode 100644 libs/user-crypto-management/tsconfig.lib.json create mode 100644 libs/user-crypto-management/tsconfig.spec.json diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index c6c1e42ae52..c2e04d94f95 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -192,6 +192,7 @@ apps/cli/src/key-management @bitwarden/team-key-management-dev bitwarden_license/bit-web/src/app/key-management @bitwarden/team-key-management-dev libs/key-management @bitwarden/team-key-management-dev libs/key-management-ui @bitwarden/team-key-management-dev +libs/user-crypto-management @bitwarden/team-key-management-dev libs/common/src/key-management @bitwarden/team-key-management-dev # Node-cryptofunction service libs/node @bitwarden/team-key-management-dev diff --git a/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.spec.ts b/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.spec.ts index a2330025c92..fec972c82f2 100644 --- a/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.spec.ts +++ b/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.spec.ts @@ -57,6 +57,7 @@ import { KeyRotationTrustInfoComponent, } from "@bitwarden/key-management-ui"; import { BitwardenClient, PureCrypto } from "@bitwarden/sdk-internal"; +import { UserKeyRotationServiceAbstraction } from "@bitwarden/user-crypto-management"; import { OrganizationUserResetPasswordService } from "../../admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service"; import { WebauthnLoginAdminService } from "../../auth"; @@ -287,6 +288,7 @@ describe("KeyRotationService", () => { let mockSdkClientFactory: MockProxy; let mockSecurityStateService: MockProxy; let mockMasterPasswordService: MockProxy; + let mockSdkUserKeyRotationService: MockProxy; const mockUser = { id: "mockUserId" as UserId, @@ -348,6 +350,7 @@ describe("KeyRotationService", () => { mockDialogService = mock(); mockCryptoFunctionService = mock(); mockKdfConfigService = mock(); + mockSdkUserKeyRotationService = mock(); mockSdkClientFactory = mock(); mockSdkClientFactory.createSdkClient.mockResolvedValue({ crypto: () => { @@ -358,6 +361,7 @@ describe("KeyRotationService", () => { } as any; }, } as BitwardenClient); + mockSecurityStateService = mock(); mockMasterPasswordService = mock(); @@ -384,6 +388,7 @@ describe("KeyRotationService", () => { mockSdkClientFactory, mockSecurityStateService, mockMasterPasswordService, + mockSdkUserKeyRotationService, ); }); @@ -509,7 +514,12 @@ describe("KeyRotationService", () => { ); mockKeyService.userSigningKey$.mockReturnValue(new BehaviorSubject(null)); mockSecurityStateService.accountSecurityState$.mockReturnValue(new BehaviorSubject(null)); - mockConfigService.getFeatureFlag.mockResolvedValue(true); + mockConfigService.getFeatureFlag.mockImplementation(async (flag: FeatureFlag) => { + if (flag === FeatureFlag.EnrollAeadOnKeyRotation) { + return true; + } + return false; + }); const spy = jest.spyOn(keyRotationService, "getRotatedAccountKeysFlagged").mockResolvedValue({ userKey: TEST_VECTOR_USER_KEY_V2, diff --git a/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.ts b/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.ts index 68253a4a35d..26dcacd8f11 100644 --- a/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.ts +++ b/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.ts @@ -39,6 +39,7 @@ import { KeyRotationTrustInfoComponent, } from "@bitwarden/key-management-ui"; import { PureCrypto, TokenProvider } from "@bitwarden/sdk-internal"; +import { UserKeyRotationServiceAbstraction } from "@bitwarden/user-crypto-management"; import { OrganizationUserResetPasswordService } from "../../admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service"; import { WebauthnLoginAdminService } from "../../auth/core"; @@ -101,6 +102,7 @@ export class UserKeyRotationService { private sdkClientFactory: SdkClientFactory, private securityStateService: SecurityStateService, private masterPasswordService: MasterPasswordServiceAbstraction, + private sdkUserKeyRotationService: UserKeyRotationServiceAbstraction, ) {} /** @@ -116,6 +118,28 @@ export class UserKeyRotationService { user: Account, newMasterPasswordHint?: string, ): Promise { + const useSdkKeyRotation = await this.configService.getFeatureFlag(FeatureFlag.SdkKeyRotation); + if (useSdkKeyRotation) { + this.logService.info( + "[UserKey Rotation] Using SDK-based key rotation service from user-crypto-management", + ); + await this.sdkUserKeyRotationService.changePasswordAndRotateUserKey( + currentMasterPassword, + newMasterPassword, + newMasterPasswordHint, + asUuid(user.id), + ); + this.toastService.showToast({ + variant: "success", + title: this.i18nService.t("rotationCompletedTitle"), + message: this.i18nService.t("rotationCompletedDesc"), + timeout: 15000, + }); + + await this.logoutService.logout(user.id); + return; + } + // Key-rotation uses the SDK, so we need to ensure that the SDK is loaded / the WASM initialized. await SdkLoadService.Ready; diff --git a/jest.config.js b/jest.config.js index bfe447f7a53..5ea699febff 100644 --- a/jest.config.js +++ b/jest.config.js @@ -61,6 +61,7 @@ module.exports = { "/libs/vault/jest.config.js", "/libs/auto-confirm/jest.config.js", "/libs/subscription/jest.config.js", + "/libs/user-crypto-management/jest.config.js", ], // Workaround for a memory leak that crashes tests in CI: diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index 7b1013077d7..6fdb146beb8 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -41,6 +41,7 @@ export enum FeatureFlag { PrivateKeyRegeneration = "pm-12241-private-key-regeneration", EnrollAeadOnKeyRotation = "enroll-aead-on-key-rotation", ForceUpdateKDFSettings = "pm-18021-force-update-kdf-settings", + SdkKeyRotation = "pm-30144-sdk-key-rotation", LinuxBiometricsV2 = "pm-26340-linux-biometrics-v2", NoLogoutOnKdfChange = "pm-23995-no-logout-on-kdf-change", PasskeyUnlock = "pm-2035-passkey-unlock", @@ -157,6 +158,7 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.PrivateKeyRegeneration]: FALSE, [FeatureFlag.EnrollAeadOnKeyRotation]: FALSE, [FeatureFlag.ForceUpdateKDFSettings]: FALSE, + [FeatureFlag.SdkKeyRotation]: FALSE, [FeatureFlag.LinuxBiometricsV2]: FALSE, [FeatureFlag.NoLogoutOnKdfChange]: FALSE, [FeatureFlag.PasskeyUnlock]: FALSE, diff --git a/libs/user-crypto-management/README.md b/libs/user-crypto-management/README.md new file mode 100644 index 00000000000..5d348f8f4bb --- /dev/null +++ b/libs/user-crypto-management/README.md @@ -0,0 +1,5 @@ +# user-crypto-management + +Owned by: key-management + +Manage a user's cryptography and cryptographic settings diff --git a/libs/user-crypto-management/eslint.config.mjs b/libs/user-crypto-management/eslint.config.mjs new file mode 100644 index 00000000000..9c37d10e3ff --- /dev/null +++ b/libs/user-crypto-management/eslint.config.mjs @@ -0,0 +1,3 @@ +import baseConfig from "../../eslint.config.mjs"; + +export default [...baseConfig]; diff --git a/libs/user-crypto-management/jest.config.js b/libs/user-crypto-management/jest.config.js new file mode 100644 index 00000000000..886da6c0940 --- /dev/null +++ b/libs/user-crypto-management/jest.config.js @@ -0,0 +1,11 @@ +module.exports = { + displayName: "user-crypto-management", + preset: "../../jest.preset.js", + testEnvironment: "node", + setupFilesAfterEnv: ["/test.setup.ts"], + transform: { + "^.+\\.[tj]s$": ["ts-jest", { tsconfig: "/tsconfig.spec.json" }], + }, + moduleFileExtensions: ["ts", "js", "html"], + coverageDirectory: "../../coverage/libs/user-crypto-management", +}; diff --git a/libs/user-crypto-management/package.json b/libs/user-crypto-management/package.json new file mode 100644 index 00000000000..d71b90f7cb2 --- /dev/null +++ b/libs/user-crypto-management/package.json @@ -0,0 +1,11 @@ +{ + "name": "@bitwarden/user-crypto-management", + "version": "0.0.1", + "description": "Manage a user's cryptography and cryptographic settings", + "private": true, + "type": "commonjs", + "main": "index.js", + "types": "index.d.ts", + "license": "GPL-3.0", + "author": "key-management" +} diff --git a/libs/user-crypto-management/project.json b/libs/user-crypto-management/project.json new file mode 100644 index 00000000000..548fbe55ec3 --- /dev/null +++ b/libs/user-crypto-management/project.json @@ -0,0 +1,34 @@ +{ + "name": "user-crypto-management", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/user-crypto-management/src", + "projectType": "library", + "tags": [], + "targets": { + "build": { + "executor": "@nx/js:tsc", + "outputs": ["{options.outputPath}"], + "options": { + "outputPath": "dist/libs/user-crypto-management", + "main": "libs/user-crypto-management/src/index.ts", + "tsConfig": "libs/user-crypto-management/tsconfig.lib.json", + "assets": ["libs/user-crypto-management/*.md"], + "rootDir": "libs/user-crypto-management/src" + } + }, + "lint": { + "executor": "@nx/eslint:lint", + "outputs": ["{options.outputFile}"], + "options": { + "lintFilePatterns": ["libs/user-crypto-management/**/*.ts"] + } + }, + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "libs/user-crypto-management/jest.config.js" + } + } + } +} diff --git a/libs/user-crypto-management/src/index.ts b/libs/user-crypto-management/src/index.ts new file mode 100644 index 00000000000..cc3cd58300b --- /dev/null +++ b/libs/user-crypto-management/src/index.ts @@ -0,0 +1,3 @@ +export { DefaultUserKeyRotationService as UserKeyRotationService } from "./user-key-rotation.service"; +export { UserKeyRotationService as UserKeyRotationServiceAbstraction } from "./user-key-rotation.service.abstraction"; +export { UserCryptoManagementModule } from "./user-crypto-management.module"; diff --git a/libs/user-crypto-management/src/user-crypto-management.module.ts b/libs/user-crypto-management/src/user-crypto-management.module.ts new file mode 100644 index 00000000000..8eb59ebd313 --- /dev/null +++ b/libs/user-crypto-management/src/user-crypto-management.module.ts @@ -0,0 +1,25 @@ +import { NgModule } from "@angular/core"; + +import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service"; +import { DialogService } from "@bitwarden/components"; +import { LogService } from "@bitwarden/logging"; +import { safeProvider } from "@bitwarden/ui-common"; + +import { DefaultUserKeyRotationService } from "./user-key-rotation.service"; +import { UserKeyRotationService } from "./user-key-rotation.service.abstraction"; + +/** + * Angular module that provides user crypto management services. + * This module handles key rotation and trust verification for organizations + * and emergency access users. + */ +@NgModule({ + providers: [ + safeProvider({ + provide: UserKeyRotationService, + useClass: DefaultUserKeyRotationService, + deps: [SdkService, LogService, DialogService], + }), + ], +}) +export class UserCryptoManagementModule {} diff --git a/libs/user-crypto-management/src/user-key-rotation.service.abstraction.ts b/libs/user-crypto-management/src/user-key-rotation.service.abstraction.ts new file mode 100644 index 00000000000..796af456526 --- /dev/null +++ b/libs/user-crypto-management/src/user-key-rotation.service.abstraction.ts @@ -0,0 +1,41 @@ +import { PublicKey } from "@bitwarden/sdk-internal"; +import { UserId } from "@bitwarden/user-core"; + +/** + * Result of the trust verification process. + */ +export type TrustVerificationResult = { + wasTrustDenied: boolean; + trustedOrganizationPublicKeys: PublicKey[]; + trustedEmergencyAccessUserPublicKeys: PublicKey[]; +}; + +/** + * Abstraction for the user key rotation service. + * Provides functionality to rotate user keys and verify trust for organizations + * and emergency access users. + */ +export abstract class UserKeyRotationService { + /** + * Rotates the user key using the SDK, re-encrypting all required data with the new key. + * @param currentMasterPassword The current master password + * @param newMasterPassword The new master password + * @param hint Optional hint for the new master password + * @param userId The user account ID + */ + abstract changePasswordAndRotateUserKey( + currentMasterPassword: string, + newMasterPassword: string, + hint: string | undefined, + userId: UserId, + ): Promise; + + /** + * Verifies the trust of organizations and emergency access users by prompting the user. + * Since organizations and emergency access grantees are not signed, manual trust prompts + * are required to verify that the server does not inject public keys. + * @param user The user account + * @returns TrustVerificationResult containing whether trust was denied and the trusted public keys + */ + abstract verifyTrust(userId: UserId): Promise; +} diff --git a/libs/user-crypto-management/src/user-key-rotation.service.spec.ts b/libs/user-crypto-management/src/user-key-rotation.service.spec.ts new file mode 100644 index 00000000000..25b99fc979a --- /dev/null +++ b/libs/user-crypto-management/src/user-key-rotation.service.spec.ts @@ -0,0 +1,295 @@ +import { mock, MockProxy } from "jest-mock-extended"; +import { BehaviorSubject, of } from "rxjs"; + +import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service"; +import { DialogService } from "@bitwarden/components"; +import { + AccountRecoveryTrustComponent, + EmergencyAccessTrustComponent, + KeyRotationTrustInfoComponent, +} from "@bitwarden/key-management-ui"; +import { LogService } from "@bitwarden/logging"; +import { UserId } from "@bitwarden/user-core"; + +import { DefaultUserKeyRotationService } from "./user-key-rotation.service"; + +// Mock dialog open functions +const initialPromptedOpenTrue = jest.fn(); +initialPromptedOpenTrue.mockReturnValue({ closed: new BehaviorSubject(true) }); + +const initialPromptedOpenFalse = jest.fn(); +initialPromptedOpenFalse.mockReturnValue({ closed: new BehaviorSubject(false) }); + +const emergencyAccessTrustOpenTrusted = jest.fn(); +emergencyAccessTrustOpenTrusted.mockReturnValue({ + closed: new BehaviorSubject(true), +}); + +const emergencyAccessTrustOpenUntrusted = jest.fn(); +emergencyAccessTrustOpenUntrusted.mockReturnValue({ + closed: new BehaviorSubject(false), +}); + +const accountRecoveryTrustOpenTrusted = jest.fn(); +accountRecoveryTrustOpenTrusted.mockReturnValue({ + closed: new BehaviorSubject(true), +}); + +const accountRecoveryTrustOpenUntrusted = jest.fn(); +accountRecoveryTrustOpenUntrusted.mockReturnValue({ + closed: new BehaviorSubject(false), +}); + +// Mock the key-management-ui module before importing components +jest.mock("@bitwarden/key-management-ui", () => ({ + KeyRotationTrustInfoComponent: { + open: jest.fn(), + }, + EmergencyAccessTrustComponent: { + open: jest.fn(), + }, + AccountRecoveryTrustComponent: { + open: jest.fn(), + }, +})); + +describe("DefaultUserKeyRotationService", () => { + let service: DefaultUserKeyRotationService; + + let mockSdkService: MockProxy; + let mockLogService: MockProxy; + let mockDialogService: MockProxy; + + const mockUserId = "mockUserId" as UserId; + + let mockUserCryptoManagement: { + get_untrusted_emergency_access_public_keys: jest.Mock; + get_untrusted_organization_public_keys: jest.Mock; + }; + + beforeEach(() => { + jest.clearAllMocks(); + + mockSdkService = mock(); + mockLogService = mock(); + mockDialogService = mock(); + + mockUserCryptoManagement = { + get_untrusted_emergency_access_public_keys: jest.fn(), + get_untrusted_organization_public_keys: jest.fn(), + }; + + mockUserCryptoManagement.get_untrusted_emergency_access_public_keys.mockResolvedValue([]); + mockUserCryptoManagement.get_untrusted_organization_public_keys.mockResolvedValue([]); + + const mockSdkClient = { + take: jest.fn().mockReturnValue({ + value: { + user_crypto_management: () => mockUserCryptoManagement, + }, + [Symbol.dispose]: jest.fn(), + }), + }; + + mockSdkService.userClient$.mockReturnValue(of(mockSdkClient as any)); + + service = new DefaultUserKeyRotationService(mockSdkService, mockLogService, mockDialogService); + + KeyRotationTrustInfoComponent.open = initialPromptedOpenTrue; + EmergencyAccessTrustComponent.open = emergencyAccessTrustOpenTrusted; + AccountRecoveryTrustComponent.open = accountRecoveryTrustOpenTrusted; + }); + + describe("verifyTrust", () => { + const mockEmergencyAccessMembership = { + id: "mockId", + name: "mockName", + public_key: new Uint8Array([1, 2, 3]), + }; + + const mockOrganizationMembership = { + organization_id: "mockOrgId", + name: "mockOrgName", + public_key: new Uint8Array([4, 5, 6]), + }; + + it("returns empty arrays if initial dialog is closed", async () => { + KeyRotationTrustInfoComponent.open = initialPromptedOpenFalse; + mockUserCryptoManagement.get_untrusted_emergency_access_public_keys.mockResolvedValue([ + mockEmergencyAccessMembership, + ]); + mockUserCryptoManagement.get_untrusted_organization_public_keys.mockResolvedValue([ + mockOrganizationMembership, + ]); + + const { + wasTrustDenied, + trustedOrganizationPublicKeys: trustedOrgs, + trustedEmergencyAccessUserPublicKeys: trustedEmergencyAccessUsers, + } = await service.verifyTrust(mockUserId); + + expect(trustedEmergencyAccessUsers).toEqual([]); + expect(trustedOrgs).toEqual([]); + expect(wasTrustDenied).toBe(true); + }); + + it("returns empty arrays if account recovery dialog is closed", async () => { + KeyRotationTrustInfoComponent.open = initialPromptedOpenTrue; + AccountRecoveryTrustComponent.open = accountRecoveryTrustOpenUntrusted; + mockUserCryptoManagement.get_untrusted_emergency_access_public_keys.mockResolvedValue([ + mockEmergencyAccessMembership, + ]); + mockUserCryptoManagement.get_untrusted_organization_public_keys.mockResolvedValue([ + mockOrganizationMembership, + ]); + + const { + wasTrustDenied, + trustedOrganizationPublicKeys: trustedOrgs, + trustedEmergencyAccessUserPublicKeys: trustedEmergencyAccessUsers, + } = await service.verifyTrust(mockUserId); + + expect(trustedEmergencyAccessUsers).toEqual([]); + expect(trustedOrgs).toEqual([]); + expect(wasTrustDenied).toBe(true); + }); + + it("returns empty arrays if emergency access dialog is closed", async () => { + KeyRotationTrustInfoComponent.open = initialPromptedOpenTrue; + AccountRecoveryTrustComponent.open = accountRecoveryTrustOpenTrusted; + EmergencyAccessTrustComponent.open = emergencyAccessTrustOpenUntrusted; + mockUserCryptoManagement.get_untrusted_emergency_access_public_keys.mockResolvedValue([ + mockEmergencyAccessMembership, + ]); + mockUserCryptoManagement.get_untrusted_organization_public_keys.mockResolvedValue([]); + + const { + wasTrustDenied, + trustedOrganizationPublicKeys: trustedOrgs, + trustedEmergencyAccessUserPublicKeys: trustedEmergencyAccessUsers, + } = await service.verifyTrust(mockUserId); + + expect(trustedEmergencyAccessUsers).toEqual([]); + expect(trustedOrgs).toEqual([]); + expect(wasTrustDenied).toBe(true); + }); + + it("returns trusted keys when all dialogs are confirmed with only emergency access users", async () => { + KeyRotationTrustInfoComponent.open = initialPromptedOpenTrue; + EmergencyAccessTrustComponent.open = emergencyAccessTrustOpenTrusted; + AccountRecoveryTrustComponent.open = accountRecoveryTrustOpenTrusted; + mockUserCryptoManagement.get_untrusted_emergency_access_public_keys.mockResolvedValue([ + mockEmergencyAccessMembership, + ]); + mockUserCryptoManagement.get_untrusted_organization_public_keys.mockResolvedValue([]); + + const { + wasTrustDenied, + trustedOrganizationPublicKeys: trustedOrgs, + trustedEmergencyAccessUserPublicKeys: trustedEmergencyAccessUsers, + } = await service.verifyTrust(mockUserId); + + expect(wasTrustDenied).toBe(false); + expect(trustedEmergencyAccessUsers).toEqual([mockEmergencyAccessMembership.public_key]); + expect(trustedOrgs).toEqual([]); + }); + + it("returns trusted keys when all dialogs are confirmed with only organizations", async () => { + KeyRotationTrustInfoComponent.open = initialPromptedOpenTrue; + EmergencyAccessTrustComponent.open = emergencyAccessTrustOpenTrusted; + AccountRecoveryTrustComponent.open = accountRecoveryTrustOpenTrusted; + mockUserCryptoManagement.get_untrusted_emergency_access_public_keys.mockResolvedValue([]); + mockUserCryptoManagement.get_untrusted_organization_public_keys.mockResolvedValue([ + mockOrganizationMembership, + ]); + + const { + wasTrustDenied, + trustedOrganizationPublicKeys: trustedOrgs, + trustedEmergencyAccessUserPublicKeys: trustedEmergencyAccessUsers, + } = await service.verifyTrust(mockUserId); + + expect(wasTrustDenied).toBe(false); + expect(trustedEmergencyAccessUsers).toEqual([]); + expect(trustedOrgs).toEqual([mockOrganizationMembership.public_key]); + }); + + it("returns empty arrays when no organizations or emergency access users exist", async () => { + mockUserCryptoManagement.get_untrusted_emergency_access_public_keys.mockResolvedValue([]); + mockUserCryptoManagement.get_untrusted_organization_public_keys.mockResolvedValue([]); + + const { + wasTrustDenied, + trustedOrganizationPublicKeys: trustedOrgs, + trustedEmergencyAccessUserPublicKeys: trustedEmergencyAccessUsers, + } = await service.verifyTrust(mockUserId); + + expect(wasTrustDenied).toBe(false); + expect(trustedEmergencyAccessUsers).toEqual([]); + expect(trustedOrgs).toEqual([]); + }); + + it("returns trusted keys when all dialogs are confirmed with both organizations and emergency access users", async () => { + KeyRotationTrustInfoComponent.open = initialPromptedOpenTrue; + EmergencyAccessTrustComponent.open = emergencyAccessTrustOpenTrusted; + AccountRecoveryTrustComponent.open = accountRecoveryTrustOpenTrusted; + mockUserCryptoManagement.get_untrusted_emergency_access_public_keys.mockResolvedValue([ + mockEmergencyAccessMembership, + ]); + mockUserCryptoManagement.get_untrusted_organization_public_keys.mockResolvedValue([ + mockOrganizationMembership, + ]); + + const { + wasTrustDenied, + trustedOrganizationPublicKeys: trustedOrgs, + trustedEmergencyAccessUserPublicKeys: trustedEmergencyAccessUsers, + } = await service.verifyTrust(mockUserId); + + expect(wasTrustDenied).toBe(false); + expect(trustedEmergencyAccessUsers).toEqual([mockEmergencyAccessMembership.public_key]); + expect(trustedOrgs).toEqual([mockOrganizationMembership.public_key]); + }); + + it("does not show initial dialog when no organizations or emergency access users exist", async () => { + mockUserCryptoManagement.get_untrusted_emergency_access_public_keys.mockResolvedValue([]); + mockUserCryptoManagement.get_untrusted_organization_public_keys.mockResolvedValue([]); + + await service.verifyTrust(mockUserId); + + expect(KeyRotationTrustInfoComponent.open).not.toHaveBeenCalled(); + }); + + it("shows initial dialog when organizations exist", async () => { + KeyRotationTrustInfoComponent.open = initialPromptedOpenTrue; + AccountRecoveryTrustComponent.open = accountRecoveryTrustOpenTrusted; + mockUserCryptoManagement.get_untrusted_emergency_access_public_keys.mockResolvedValue([]); + mockUserCryptoManagement.get_untrusted_organization_public_keys.mockResolvedValue([ + mockOrganizationMembership, + ]); + + await service.verifyTrust(mockUserId); + + expect(KeyRotationTrustInfoComponent.open).toHaveBeenCalledWith(mockDialogService, { + numberOfEmergencyAccessUsers: 0, + orgName: mockOrganizationMembership.name, + }); + }); + + it("shows initial dialog when emergency access users exist", async () => { + KeyRotationTrustInfoComponent.open = initialPromptedOpenTrue; + EmergencyAccessTrustComponent.open = emergencyAccessTrustOpenTrusted; + mockUserCryptoManagement.get_untrusted_emergency_access_public_keys.mockResolvedValue([ + mockEmergencyAccessMembership, + ]); + mockUserCryptoManagement.get_untrusted_organization_public_keys.mockResolvedValue([]); + + await service.verifyTrust(mockUserId); + + expect(KeyRotationTrustInfoComponent.open).toHaveBeenCalledWith(mockDialogService, { + numberOfEmergencyAccessUsers: 1, + orgName: undefined, + }); + }); + }); +}); diff --git a/libs/user-crypto-management/src/user-key-rotation.service.ts b/libs/user-crypto-management/src/user-key-rotation.service.ts new file mode 100644 index 00000000000..a1af0f7f80e --- /dev/null +++ b/libs/user-crypto-management/src/user-key-rotation.service.ts @@ -0,0 +1,164 @@ +import { catchError, EMPTY, firstValueFrom, map } from "rxjs"; + +import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { DialogService } from "@bitwarden/components"; +import { + AccountRecoveryTrustComponent, + EmergencyAccessTrustComponent, + KeyRotationTrustInfoComponent, +} from "@bitwarden/key-management-ui"; +import { LogService } from "@bitwarden/logging"; +import { RotateUserKeysRequest } from "@bitwarden/sdk-internal"; +import { UserId } from "@bitwarden/user-core"; + +import { + TrustVerificationResult, + UserKeyRotationService, +} from "./user-key-rotation.service.abstraction"; + +/** + * Service for rotating user keys using the SDK. + * Handles key rotation and trust verification for organizations and emergency access users. + */ +export class DefaultUserKeyRotationService implements UserKeyRotationService { + constructor( + private sdkService: SdkService, + private logService: LogService, + private dialogService: DialogService, + ) {} + + async changePasswordAndRotateUserKey( + currentMasterPassword: string, + newMasterPassword: string, + hint: string | undefined, + userId: UserId, + ): Promise { + // First, the provided organizations and emergency access users need to be verified; + // this is currently done by providing the user a manual confirmation dialog. + const { wasTrustDenied, trustedOrganizationPublicKeys, trustedEmergencyAccessUserPublicKeys } = + await this.verifyTrust(userId); + if (wasTrustDenied) { + this.logService.info("[Userkey rotation] Trust was denied by user. Aborting!"); + return; + } + + return firstValueFrom( + this.sdkService.userClient$(userId).pipe( + map(async (sdk) => { + if (!sdk) { + throw new Error("SDK not available"); + } + + using ref = sdk.take(); + this.logService.info("[UserKey Rotation] Re-encrypting user data with new user key..."); + await ref.value.user_crypto_management().rotate_user_keys({ + master_key_unlock_method: { + Password: { + old_password: currentMasterPassword, + password: newMasterPassword, + hint: hint, + }, + }, + trusted_emergency_access_public_keys: trustedEmergencyAccessUserPublicKeys, + trusted_organization_public_keys: trustedOrganizationPublicKeys, + } as RotateUserKeysRequest); + }), + catchError((error: unknown) => { + this.logService.error(`Failed to rotate user keys: ${error}`); + return EMPTY; + }), + ), + ); + } + + async verifyTrust(userId: UserId): Promise { + // Since currently the joined organizations and emergency access grantees are + // not signed, manual trust prompts are required, to verify that the server + // does not inject public keys here. + // + // Once signing is implemented, this is the place to also sign the keys and + // upload the signed trust claims. + // + // The flow works in 3 steps: + // 1. Prepare the user by showing them a dialog telling them they'll be asked + // to verify the trust of their organizations and emergency access users. + // 2. Show the user a dialog for each organization and ask them to verify the trust. + // 3. Show the user a dialog for each emergency access user and ask them to verify the trust. + this.logService.info("[Userkey rotation] Verifying trust..."); + const [emergencyAccessV1Memberships, organizationV1Memberships] = await firstValueFrom( + this.sdkService.userClient$(userId).pipe( + map(async (sdk) => { + if (!sdk) { + throw new Error("SDK not available"); + } + + using ref = sdk.take(); + const emergencyAccessV1Memberships = await ref.value + .user_crypto_management() + .get_untrusted_emergency_access_public_keys(); + const organizationV1Memberships = await ref.value + .user_crypto_management() + .get_untrusted_organization_public_keys(); + return [emergencyAccessV1Memberships, organizationV1Memberships] as const; + }), + ), + ); + this.logService.info("result", { emergencyAccessV1Memberships, organizationV1Memberships }); + + if (organizationV1Memberships.length > 0 || emergencyAccessV1Memberships.length > 0) { + this.logService.info("[Userkey rotation] Showing trust info dialog..."); + const trustInfoDialog = KeyRotationTrustInfoComponent.open(this.dialogService, { + numberOfEmergencyAccessUsers: emergencyAccessV1Memberships.length, + orgName: + organizationV1Memberships.length > 0 ? organizationV1Memberships[0].name : undefined, + }); + if (!(await firstValueFrom(trustInfoDialog.closed))) { + return { + wasTrustDenied: true, + trustedOrganizationPublicKeys: [], + trustedEmergencyAccessUserPublicKeys: [], + }; + } + } + + for (const organization of organizationV1Memberships) { + const dialogRef = AccountRecoveryTrustComponent.open(this.dialogService, { + name: organization.name, + orgId: organization.organization_id as string, + publicKey: Utils.fromB64ToArray(organization.public_key), + }); + if (!(await firstValueFrom(dialogRef.closed))) { + return { + wasTrustDenied: true, + trustedOrganizationPublicKeys: [], + trustedEmergencyAccessUserPublicKeys: [], + }; + } + } + + for (const details of emergencyAccessV1Memberships) { + const dialogRef = EmergencyAccessTrustComponent.open(this.dialogService, { + name: details.name, + userId: details.id as string, + publicKey: Utils.fromB64ToArray(details.public_key), + }); + if (!(await firstValueFrom(dialogRef.closed))) { + return { + wasTrustDenied: true, + trustedOrganizationPublicKeys: [], + trustedEmergencyAccessUserPublicKeys: [], + }; + } + } + + this.logService.info( + "[Userkey rotation] Trust verified for all organizations and emergency access users", + ); + return { + wasTrustDenied: false, + trustedOrganizationPublicKeys: organizationV1Memberships.map((d) => d.public_key), + trustedEmergencyAccessUserPublicKeys: emergencyAccessV1Memberships.map((d) => d.public_key), + }; + } +} diff --git a/libs/user-crypto-management/test.setup.ts b/libs/user-crypto-management/test.setup.ts new file mode 100644 index 00000000000..f0cf585fdd8 --- /dev/null +++ b/libs/user-crypto-management/test.setup.ts @@ -0,0 +1 @@ +import "core-js/proposals/explicit-resource-management"; diff --git a/libs/user-crypto-management/tsconfig.eslint.json b/libs/user-crypto-management/tsconfig.eslint.json new file mode 100644 index 00000000000..3daf120441a --- /dev/null +++ b/libs/user-crypto-management/tsconfig.eslint.json @@ -0,0 +1,6 @@ +{ + "extends": "../../tsconfig.base.json", + "files": [], + "include": ["src/**/*.ts", "src/**/*.js"], + "exclude": ["**/build", "**/dist"] +} diff --git a/libs/user-crypto-management/tsconfig.json b/libs/user-crypto-management/tsconfig.json new file mode 100644 index 00000000000..9c607a26b09 --- /dev/null +++ b/libs/user-crypto-management/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../../tsconfig.base", + "include": ["src", "spec"], + "exclude": ["node_modules", "dist"] +} diff --git a/libs/user-crypto-management/tsconfig.lib.json b/libs/user-crypto-management/tsconfig.lib.json new file mode 100644 index 00000000000..9cbf6736007 --- /dev/null +++ b/libs/user-crypto-management/tsconfig.lib.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "declaration": true, + "types": ["node"] + }, + "include": ["src/**/*.ts"], + "exclude": ["jest.config.js", "src/**/*.spec.ts"] +} diff --git a/libs/user-crypto-management/tsconfig.spec.json b/libs/user-crypto-management/tsconfig.spec.json new file mode 100644 index 00000000000..1275f148a18 --- /dev/null +++ b/libs/user-crypto-management/tsconfig.spec.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "module": "commonjs", + "moduleResolution": "node10", + "types": ["jest", "node"] + }, + "include": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts", "src/**/*.d.ts"] +} diff --git a/package-lock.json b/package-lock.json index 9f6e82d98ef..f5ac6ccbc0c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -642,6 +642,11 @@ "version": "0.0.0", "license": "GPL-3.0" }, + "libs/user-crypto-management": { + "name": "@bitwarden/user-crypto-management", + "version": "0.0.1", + "license": "GPL-3.0" + }, "libs/vault": { "name": "@bitwarden/vault", "version": "0.0.0", @@ -5101,6 +5106,10 @@ "resolved": "libs/user-core", "link": true }, + "node_modules/@bitwarden/user-crypto-management": { + "resolved": "libs/user-crypto-management", + "link": true + }, "node_modules/@bitwarden/vault": { "resolved": "libs/vault", "link": true diff --git a/tsconfig.base.json b/tsconfig.base.json index 995eac031fd..fb76ea752a7 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -33,6 +33,7 @@ "@bitwarden/client-type": ["./libs/client-type/src/index.ts"], "@bitwarden/common/spec": ["./libs/common/spec"], "@bitwarden/common/*": ["./libs/common/src/*"], + "@bitwarden/common/spec": ["./libs/common/spec"], "@bitwarden/components": ["./libs/components/src"], "@bitwarden/core-test-utils": ["./libs/core-test-utils/src/index.ts"], "@bitwarden/dirt-card": ["./libs/dirt/card/src"], @@ -64,6 +65,7 @@ "@bitwarden/ui-common": ["./libs/ui/common/src"], "@bitwarden/ui-common/setup-jest": ["./libs/ui/common/src/setup-jest"], "@bitwarden/user-core": ["./libs/user-core/src/index.ts"], + "@bitwarden/user-crypto-management": ["./libs/user-crypto-management/src/index.ts"], "@bitwarden/vault": ["./libs/vault/src"], "@bitwarden/vault-export-core": ["./libs/tools/export/vault-export/vault-export-core/src"], "@bitwarden/vault-export-ui": ["./libs/tools/export/vault-export/vault-export-ui/src"], From 767caa431275a2c3fe6c1ca7b1c6ced8fa7932aa Mon Sep 17 00:00:00 2001 From: John Harrington <84741727+harr1424@users.noreply.github.com> Date: Fri, 20 Feb 2026 07:51:05 -0700 Subject: [PATCH 42/89] [PM-32472] [Defect] Generator page will not display on desktop (#19085) * remove redundant link and import * apply lost styles --- .../generator/credential-generator.component.html | 11 +++++++---- .../tools/generator/credential-generator.component.ts | 10 ++-------- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/apps/desktop/src/app/tools/generator/credential-generator.component.html b/apps/desktop/src/app/tools/generator/credential-generator.component.html index 12088a147c5..241d21b1bb7 100644 --- a/apps/desktop/src/app/tools/generator/credential-generator.component.html +++ b/apps/desktop/src/app/tools/generator/credential-generator.component.html @@ -5,14 +5,17 @@ diff --git a/apps/desktop/src/app/tools/generator/credential-generator.component.ts b/apps/desktop/src/app/tools/generator/credential-generator.component.ts index 42313c48f7f..036a5e104aa 100644 --- a/apps/desktop/src/app/tools/generator/credential-generator.component.ts +++ b/apps/desktop/src/app/tools/generator/credential-generator.component.ts @@ -1,13 +1,7 @@ import { Component } from "@angular/core"; import { JslibModule } from "@bitwarden/angular/jslib.module"; -import { - ButtonModule, - DialogModule, - DialogService, - ItemModule, - LinkModule, -} from "@bitwarden/components"; +import { ButtonModule, DialogModule, DialogService, ItemModule } from "@bitwarden/components"; import { CredentialGeneratorHistoryDialogComponent, GeneratorModule, @@ -18,7 +12,7 @@ import { @Component({ selector: "credential-generator", templateUrl: "credential-generator.component.html", - imports: [DialogModule, ButtonModule, JslibModule, GeneratorModule, ItemModule, LinkModule], + imports: [DialogModule, ButtonModule, JslibModule, GeneratorModule, ItemModule], }) export class CredentialGeneratorComponent { constructor(private dialogService: DialogService) {} From aa4eac7d409cd86fd74e680932d2f7e49f85057e Mon Sep 17 00:00:00 2001 From: Jonathan Prusik Date: Fri, 20 Feb 2026 10:01:04 -0500 Subject: [PATCH 43/89] do not show passkey dialog and notifications at the same time (#18878) --- .../notification.background.spec.ts | 21 ++++- .../background/notification.background.ts | 7 ++ .../abstractions/fido2.background.ts | 2 + .../fido2/background/fido2.background.spec.ts | 78 +++++++++++++++++++ .../fido2/background/fido2.background.ts | 34 +++++--- .../browser-fido2-user-interface.service.ts | 3 + .../browser/src/background/main.background.ts | 1 + 7 files changed, 136 insertions(+), 10 deletions(-) diff --git a/apps/browser/src/autofill/background/notification.background.spec.ts b/apps/browser/src/autofill/background/notification.background.spec.ts index 95d4111987b..1bd1ae5513b 100644 --- a/apps/browser/src/autofill/background/notification.background.spec.ts +++ b/apps/browser/src/autofill/background/notification.background.spec.ts @@ -28,6 +28,7 @@ import { TaskService, SecurityTask } from "@bitwarden/common/vault/tasks"; import { BrowserApi } from "../../platform/browser/browser-api"; import { NotificationType } from "../enums/notification-type.enum"; +import { Fido2Background } from "../fido2/background/abstractions/fido2.background"; import { FormData } from "../services/abstractions/autofill.service"; import AutofillService from "../services/autofill.service"; import { createAutofillPageDetailsMock, createChromeTabMock } from "../spec/autofill-mocks"; @@ -81,6 +82,8 @@ describe("NotificationBackground", () => { const configService = mock(); const accountService = mock(); const organizationService = mock(); + const fido2Background = mock(); + fido2Background.isCredentialRequestInProgress.mockReturnValue(false); const userId = "testId" as UserId; const activeAccountSubject = new BehaviorSubject({ @@ -115,6 +118,7 @@ describe("NotificationBackground", () => { userNotificationSettingsService, taskService, messagingService, + fido2Background, ); }); @@ -759,7 +763,6 @@ describe("NotificationBackground", () => { notificationBackground as any, "getEnableChangedPasswordPrompt", ); - pushChangePasswordToQueueSpy = jest.spyOn( notificationBackground as any, "pushChangePasswordToQueue", @@ -822,6 +825,22 @@ describe("NotificationBackground", () => { expectSkippedCheckingNotification(); }); + it("skips checking if a notification should trigger if a fido2 credential request is in progress for the tab", async () => { + const formEntryData: ModifyLoginCipherFormData = { + newPassword: "", + password: "", + uri: mockFormURI, + username: "ADent", + }; + + activeAccountStatusMock$.next(AuthenticationStatus.Unlocked); + fido2Background.isCredentialRequestInProgress.mockReturnValueOnce(true); + + await notificationBackground.triggerCipherNotification(formEntryData, tab); + + expectSkippedCheckingNotification(); + }); + it("skips checking if a notification should trigger if the user has disabled both the new login and update password notification", async () => { const formEntryData: ModifyLoginCipherFormData = { newPassword: "Bab3lPhs5h", diff --git a/apps/browser/src/autofill/background/notification.background.ts b/apps/browser/src/autofill/background/notification.background.ts index 3713cd7c4c2..64c52701e21 100644 --- a/apps/browser/src/autofill/background/notification.background.ts +++ b/apps/browser/src/autofill/background/notification.background.ts @@ -61,6 +61,7 @@ import { } from "../content/components/cipher/types"; import { CollectionView } from "../content/components/common-types"; import { NotificationType } from "../enums/notification-type.enum"; +import { Fido2Background } from "../fido2/background/abstractions/fido2.background"; import { AutofillService } from "../services/abstractions/autofill.service"; import { TemporaryNotificationChangeLoginService } from "../services/notification-change-login-password.service"; @@ -165,6 +166,7 @@ export default class NotificationBackground { private userNotificationSettingsService: UserNotificationSettingsServiceAbstraction, private taskService: TaskService, protected messagingService: MessagingService, + private fido2Background: Fido2Background, ) {} init() { @@ -665,6 +667,11 @@ export default class NotificationBackground { return false; } + // If there is an active passkey prompt, exit early + if (this.fido2Background.isCredentialRequestInProgress(tab.id)) { + return false; + } + // If no cipher add/update notifications are enabled, we can exit early const changePasswordNotificationIsEnabled = await this.getEnableChangedPasswordPrompt(); const newLoginNotificationIsEnabled = await this.getEnableAddedLoginPrompt(); diff --git a/apps/browser/src/autofill/fido2/background/abstractions/fido2.background.ts b/apps/browser/src/autofill/fido2/background/abstractions/fido2.background.ts index 6ad069ad56e..c5346d61566 100644 --- a/apps/browser/src/autofill/fido2/background/abstractions/fido2.background.ts +++ b/apps/browser/src/autofill/fido2/background/abstractions/fido2.background.ts @@ -45,6 +45,8 @@ type Fido2BackgroundExtensionMessageHandlers = { interface Fido2Background { init(): void; + isCredentialRequestInProgress(tabId: number): boolean; + isPasskeySettingEnabled(): Promise; } export { diff --git a/apps/browser/src/autofill/fido2/background/fido2.background.spec.ts b/apps/browser/src/autofill/fido2/background/fido2.background.spec.ts index 752851b3d37..6ead7416b96 100644 --- a/apps/browser/src/autofill/fido2/background/fido2.background.spec.ts +++ b/apps/browser/src/autofill/fido2/background/fido2.background.spec.ts @@ -256,6 +256,84 @@ describe("Fido2Background", () => { }); }); + describe("isCredentialRequestInProgress", () => { + beforeEach(() => { + fido2Background.init(); + }); + + it("returns false when no credential request is active", () => { + expect(fido2Background.isCredentialRequestInProgress(tabMock.id)).toBe(false); + }); + + it("returns true while a register credential request is in progress", async () => { + let duringRequestResult: boolean; + fido2ClientService.createCredential.mockImplementation(async () => { + duringRequestResult = fido2Background.isCredentialRequestInProgress(tabMock.id); + return mock(); + }); + + const message = mock({ + command: "fido2RegisterCredentialRequest", + requestId: "123", + data: mock(), + }); + + sendMockExtensionMessage(message, senderMock); + await flushPromises(); + + expect(duringRequestResult).toBe(true); + }); + + it("returns true while a get credential request is in progress", async () => { + let duringRequestResult: boolean; + fido2ClientService.assertCredential.mockImplementation(async () => { + duringRequestResult = fido2Background.isCredentialRequestInProgress(tabMock.id); + return mock(); + }); + + const message = mock({ + command: "fido2GetCredentialRequest", + requestId: "123", + data: mock(), + }); + + sendMockExtensionMessage(message, senderMock); + await flushPromises(); + + expect(duringRequestResult).toBe(true); + }); + + it("returns false after a credential request completes", async () => { + fido2ClientService.createCredential.mockResolvedValue(mock()); + + const message = mock({ + command: "fido2RegisterCredentialRequest", + requestId: "123", + data: mock(), + }); + + sendMockExtensionMessage(message, senderMock); + await flushPromises(); + + expect(fido2Background.isCredentialRequestInProgress(tabMock.id)).toBe(false); + }); + + it("returns false after a credential request errors", async () => { + fido2ClientService.createCredential.mockRejectedValue(new Error("error")); + + const message = mock({ + command: "fido2RegisterCredentialRequest", + requestId: "123", + data: mock(), + }); + + sendMockExtensionMessage(message, senderMock); + await flushPromises(); + + expect(fido2Background.isCredentialRequestInProgress(tabMock.id)).toBe(false); + }); + }); + describe("extension message handlers", () => { beforeEach(() => { fido2Background.init(); diff --git a/apps/browser/src/autofill/fido2/background/fido2.background.ts b/apps/browser/src/autofill/fido2/background/fido2.background.ts index 22ee4a1822d..495b0d85f0b 100644 --- a/apps/browser/src/autofill/fido2/background/fido2.background.ts +++ b/apps/browser/src/autofill/fido2/background/fido2.background.ts @@ -35,6 +35,7 @@ export class Fido2Background implements Fido2BackgroundInterface { private currentAuthStatus$: Subscription; private abortManager = new AbortManager(); private fido2ContentScriptPortsSet = new Set(); + private activeCredentialRequests = new Set(); private registeredContentScripts: browser.contentScripts.RegisteredContentScript; private readonly sharedInjectionDetails: SharedFido2ScriptInjectionDetails = { runAt: "document_start", @@ -61,6 +62,16 @@ export class Fido2Background implements Fido2BackgroundInterface { private authService: AuthService, ) {} + /** + * Checks if a FIDO2 credential request (registration or assertion) + * is currently in progress for the given tab. + * + * @param tabId - The tab id to check. + */ + isCredentialRequestInProgress(tabId: number): boolean { + return this.activeCredentialRequests.has(tabId); + } + /** * Initializes the FIDO2 background service. Sets up the extension message * and port listeners. Subscribes to the enablePasskeys$ observable to @@ -307,20 +318,25 @@ export class Fido2Background implements Fido2BackgroundInterface { abortController: AbortController, ) => Promise, ) => { - return await this.abortManager.runWithAbortController(requestId, async (abortController) => { - try { - return await callback(data, tab, abortController); - } finally { - await BrowserApi.focusTab(tab.id); - await BrowserApi.focusWindow(tab.windowId); - } - }); + this.activeCredentialRequests.add(tab.id); + try { + return await this.abortManager.runWithAbortController(requestId, async (abortController) => { + try { + return await callback(data, tab, abortController); + } finally { + await BrowserApi.focusTab(tab.id); + await BrowserApi.focusWindow(tab.windowId); + } + }); + } finally { + this.activeCredentialRequests.delete(tab.id); + } }; /** * Checks if the enablePasskeys setting is enabled. */ - private async isPasskeySettingEnabled() { + async isPasskeySettingEnabled() { return await firstValueFrom(this.vaultSettingsService.enablePasskeys$); } diff --git a/apps/browser/src/autofill/fido2/services/browser-fido2-user-interface.service.ts b/apps/browser/src/autofill/fido2/services/browser-fido2-user-interface.service.ts index 19c1dbc8790..11dc170db16 100644 --- a/apps/browser/src/autofill/fido2/services/browser-fido2-user-interface.service.ts +++ b/apps/browser/src/autofill/fido2/services/browser-fido2-user-interface.service.ts @@ -363,6 +363,9 @@ export class BrowserFido2UserInterfaceSession implements Fido2UserInterfaceSessi ), ); + // Defensive measure in case an existing notification appeared before the passkey popout + await BrowserApi.tabSendMessageData(this.tab, "closeNotificationBar"); + const popoutId = await openFido2Popout(this.tab, { sessionId: this.sessionId, fallbackSupported: this.fallbackSupported, diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 25c7b344982..95ec6e5ad20 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -1409,6 +1409,7 @@ export default class MainBackground { this.userNotificationSettingsService, this.taskService, this.messagingService, + this.fido2Background, ); this.overlayNotificationsBackground = new OverlayNotificationsBackground( From e16503f0930075679d8f6002fd5989d364766099 Mon Sep 17 00:00:00 2001 From: Vicki League Date: Fri, 20 Feb 2026 10:01:38 -0500 Subject: [PATCH 44/89] [PM-24178] Handle focus when routed dialog closes in vault table (#18409) --- .../collections/vault.component.ts | 39 ++- .../navigation-switcher.component.html | 1 + .../product-switcher-content.component.html | 3 + .../vault-cipher-row.component.html | 5 + .../vault/individual-vault/vault.component.ts | 45 ++- .../src/a11y/router-focus-manager.mdx | 69 ++++ .../a11y/router-focus-manager.service.spec.ts | 323 ++++++++++++++++++ .../src/a11y/router-focus-manager.service.ts | 73 ++-- .../src/dialog/dialog/dialog.component.ts | 6 +- .../src/input/autofocus.directive.ts | 16 +- .../src/navigation/nav-item.component.html | 3 + .../src/navigation/nav-item.component.ts | 12 + .../tabs/tab-nav-bar/tab-link.component.html | 2 +- .../services/routed-vault-filter.service.ts | 2 +- 14 files changed, 546 insertions(+), 53 deletions(-) create mode 100644 libs/components/src/a11y/router-focus-manager.mdx create mode 100644 libs/components/src/a11y/router-focus-manager.service.spec.ts diff --git a/apps/web/src/app/admin-console/organizations/collections/vault.component.ts b/apps/web/src/app/admin-console/organizations/collections/vault.component.ts index a641116f4de..1d9178e6fed 100644 --- a/apps/web/src/app/admin-console/organizations/collections/vault.component.ts +++ b/apps/web/src/app/admin-console/organizations/collections/vault.component.ts @@ -1,6 +1,6 @@ import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit, ViewChild } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; -import { ActivatedRoute, Params, Router } from "@angular/router"; +import { ActivatedRoute, NavigationExtras, Params, Router } from "@angular/router"; import { BehaviorSubject, combineLatest, @@ -588,7 +588,7 @@ export class VaultComponent implements OnInit, OnDestroy { queryParamsHandling: "merge", replaceUrl: true, state: { - focusMainAfterNav: false, + focusAfterNav: false, }, }), ); @@ -812,7 +812,7 @@ export class VaultComponent implements OnInit, OnDestroy { async editCipherAttachments(cipher: CipherView) { if (cipher.reprompt !== 0 && !(await this.passwordRepromptService.showPasswordPrompt())) { - this.go({ cipherId: null, itemId: null }); + this.go({ cipherId: null, itemId: null }, this.configureRouterFocusToCipher(cipher.id)); return; } @@ -869,7 +869,7 @@ export class VaultComponent implements OnInit, OnDestroy { !(await this.passwordRepromptService.showPasswordPrompt()) ) { // didn't pass password prompt, so don't open add / edit modal - this.go({ cipherId: null, itemId: null }); + this.go({ cipherId: null, itemId: null }, this.configureRouterFocusToCipher(cipher.id)); return; } @@ -893,7 +893,10 @@ export class VaultComponent implements OnInit, OnDestroy { !(await this.passwordRepromptService.showPasswordPrompt()) ) { // Didn't pass password prompt, so don't open add / edit modal. - await this.go({ cipherId: null, itemId: null, action: null }); + await this.go( + { cipherId: null, itemId: null, action: null }, + this.configureRouterFocusToCipher(cipher.id), + ); return; } @@ -943,7 +946,10 @@ export class VaultComponent implements OnInit, OnDestroy { } // Clear the query params when the dialog closes - await this.go({ cipherId: null, itemId: null, action: null }); + await this.go( + { cipherId: null, itemId: null, action: null }, + this.configureRouterFocusToCipher(formConfig.originalCipher?.id), + ); } async cloneCipher(cipher: CipherView) { @@ -1422,7 +1428,25 @@ export class VaultComponent implements OnInit, OnDestroy { } } - private go(queryParams: any = null) { + /** + * Helper function to set up the `state.focusAfterNav` property for dialog router navigation if + * the cipherId exists. If it doesn't exist, returns undefined. + * + * This ensures that when the routed dialog is closed, the focus returns to the cipher button in + * the vault table, which allows keyboard users to continue navigating uninterrupted. + * + * @param cipherId id of cipher + * @returns Partial, specifically the state.focusAfterNav property, or undefined + */ + private configureRouterFocusToCipher(cipherId?: string): Partial | undefined { + if (cipherId) { + return { + state: { focusAfterNav: `#cipher-btn-${cipherId}` }, + }; + } + } + + private go(queryParams: any = null, navigateOptions?: NavigationExtras) { if (queryParams == null) { queryParams = { type: this.activeFilter.cipherType, @@ -1436,6 +1460,7 @@ export class VaultComponent implements OnInit, OnDestroy { queryParams: queryParams, queryParamsHandling: "merge", replaceUrl: true, + ...navigateOptions, }); } diff --git a/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.component.html b/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.component.html index 9c8f2125614..b6e06e7d06f 100644 --- a/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.component.html +++ b/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.component.html @@ -9,6 +9,7 @@ [route]="product.appRoute" [attr.icon]="product.icon" [forceActiveStyles]="product.isActive" + focusAfterNavTarget="body" > } diff --git a/apps/web/src/app/layouts/product-switcher/product-switcher-content.component.html b/apps/web/src/app/layouts/product-switcher/product-switcher-content.component.html index f2154ec74a3..290e07c932a 100644 --- a/apps/web/src/app/layouts/product-switcher/product-switcher-content.component.html +++ b/apps/web/src/app/layouts/product-switcher/product-switcher-content.component.html @@ -19,6 +19,9 @@ " class="tw-group/product-link tw-flex tw-h-24 tw-w-28 tw-flex-col tw-items-center tw-justify-center tw-rounded tw-p-1 tw-text-primary-600 tw-outline-none hover:tw-bg-background-alt hover:tw-text-primary-700 hover:tw-no-underline focus-visible:!tw-ring-2 focus-visible:!tw-ring-primary-700" ariaCurrentWhenActive="page" + [state]="{ + focusAfterNav: 'body', + }" >
+ diff --git a/apps/web/src/app/vault/individual-vault/vault.component.ts b/apps/web/src/app/vault/individual-vault/vault.component.ts index 5ff72b0d147..1f80748ab29 100644 --- a/apps/web/src/app/vault/individual-vault/vault.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault.component.ts @@ -1,5 +1,5 @@ import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit, viewChild } from "@angular/core"; -import { ActivatedRoute, Params, Router } from "@angular/router"; +import { ActivatedRoute, NavigationExtras, Params, Router } from "@angular/router"; import { combineLatest, firstValueFrom, lastValueFrom, Observable, of, Subject } from "rxjs"; import { concatMap, @@ -398,7 +398,7 @@ export class VaultComponent implements OnInit, OnDestr queryParamsHandling: "merge", replaceUrl: true, state: { - focusMainAfterNav: false, + focusAfterNav: false, }, }), ); @@ -877,7 +877,10 @@ export class VaultComponent implements OnInit, OnDestr */ async editCipherAttachments(cipher: C) { if (cipher?.reprompt !== 0 && !(await this.passwordRepromptService.showPasswordPrompt())) { - await this.go({ cipherId: null, itemId: null }); + await this.go( + { cipherId: null, itemId: null }, + this.configureRouterFocusToCipher(typeof cipher?.id === "string" ? cipher.id : undefined), + ); return; } @@ -950,7 +953,10 @@ export class VaultComponent implements OnInit, OnDestr } // Clear the query params when the dialog closes - await this.go({ cipherId: null, itemId: null, action: null }); + await this.go( + { cipherId: null, itemId: null, action: null }, + this.configureRouterFocusToCipher(formConfig.originalCipher?.id), + ); } /** @@ -1010,7 +1016,10 @@ export class VaultComponent implements OnInit, OnDestr !(await this.passwordRepromptService.showPasswordPrompt()) ) { // didn't pass password prompt, so don't open add / edit modal - await this.go({ cipherId: null, itemId: null, action: null }); + await this.go( + { cipherId: null, itemId: null, action: null }, + this.configureRouterFocusToCipher(cipher.id), + ); return; } @@ -1052,7 +1061,10 @@ export class VaultComponent implements OnInit, OnDestr !(await this.passwordRepromptService.showPasswordPrompt()) ) { // Didn't pass password prompt, so don't open add / edit modal. - await this.go({ cipherId: null, itemId: null, action: null }); + await this.go( + { cipherId: null, itemId: null, action: null }, + this.configureRouterFocusToCipher(cipher.id), + ); return; } @@ -1553,7 +1565,25 @@ export class VaultComponent implements OnInit, OnDestr this.vaultItemsComponent()?.clearSelection(); } - private async go(queryParams: any = null) { + /** + * Helper function to set up the `state.focusAfterNav` property for dialog router navigation if + * the cipherId exists. If it doesn't exist, returns undefined. + * + * This ensures that when the routed dialog is closed, the focus returns to the cipher button in + * the vault table, which allows keyboard users to continue navigating uninterrupted. + * + * @param cipherId id of cipher + * @returns Partial, specifically the state.focusAfterNav property, or undefined + */ + private configureRouterFocusToCipher(cipherId?: string): Partial | undefined { + if (cipherId) { + return { + state: { focusAfterNav: `#cipher-btn-${cipherId}` }, + }; + } + } + + private async go(queryParams: any = null, navigateOptions?: NavigationExtras) { if (queryParams == null) { queryParams = { favorites: this.activeFilter.isFavorites || null, @@ -1569,6 +1599,7 @@ export class VaultComponent implements OnInit, OnDestr queryParams: queryParams, queryParamsHandling: "merge", replaceUrl: true, + ...navigateOptions, }); } diff --git a/libs/components/src/a11y/router-focus-manager.mdx b/libs/components/src/a11y/router-focus-manager.mdx new file mode 100644 index 00000000000..aa882f9deac --- /dev/null +++ b/libs/components/src/a11y/router-focus-manager.mdx @@ -0,0 +1,69 @@ +import { Meta } from "@storybook/addon-docs/blocks"; + + + +# Router Focus Management + +On a normal non-SPA (Single Page Application) webpage, a page navigation / route change will cause +the full page to reload, and a user's focus is placed at the top of the page when the new page +loads. + +Bitwarden's Angular apps are SPAs using the Angular router to manage internal routing and page +navigation. When the Angular router performs a page navigation / route change to another internal +SPA route, the full page does not reload, and the user's focus does not move from the trigger +element unless the trigger element no longer exists. There is no other built-in notification to a +screenreader user that a navigation has occured, if the focus is not moved. + +## Web + +We handle router focus management in the web app by moving the user's focus at the end of a SPA +Angular router navigation. + +See `router-focus-manager.service.ts` for the implementation. + +### Default behavior + +By default, we focus the `main` element. + +Consumers can change or opt out of the focus management using the `state` input to the +[Angular route](https://angular.dev/api/router/RouterLink). Using `state` allows us to access the +value between browser back/forward arrows. + +### Change focus target + +In template: `` + +In typescript: `this.router.navigate([], { state: { focusAfterNav: '#selector' }})` + +Any valid `querySelector` selector can be passed. If the element is not found, no focus management +occurs as we cannot make the assumption that the default `main` element is the next best option. + +Examples of where you might want to change the target: + +- A full page navigation occurs where you do want the user to be placed at the top of the page (aka + the body element) like a non-SPA app, such as going from Password Manager to Secrets Manager +- A routed dialog needs to manually specify where the focus should return to once it is closed + +### Opt out of focus management + +In template: `` + +In typescript: `this.router.navigate([], { state: { focusAfterNav: false }})` + +Example of where you might want to manually opt out: + +- Tab component causes a route navigation, and the focus will be handled by the tab component itself + +### Autofocus directive + +Consumers can use the autofocus directive on an applicable interactive element. The autofocus +directive will take precedence over the router focus management system. See the +[Autofocus Directive docs](?path=/docs/component-library-form-autofocus-directive--docs). + +## Browser + +Not implemented yet. + +## Desktop + +Not implemented yet. diff --git a/libs/components/src/a11y/router-focus-manager.service.spec.ts b/libs/components/src/a11y/router-focus-manager.service.spec.ts new file mode 100644 index 00000000000..236a05ac038 --- /dev/null +++ b/libs/components/src/a11y/router-focus-manager.service.spec.ts @@ -0,0 +1,323 @@ +import { + computed, + DestroyRef, + EventEmitter, + Injectable, + NgZone, + Signal, + signal, +} from "@angular/core"; +import { TestBed } from "@angular/core/testing"; +import { Event, Navigation, NavigationEnd, Router } from "@angular/router"; +import { mock } from "jest-mock-extended"; +import { BehaviorSubject, Subject } from "rxjs"; + +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; + +import { RouterFocusManagerService } from "./router-focus-manager.service"; + +describe("RouterFocusManagerService", () => { + @Injectable() + class MockNgZone extends NgZone { + onStable: EventEmitter = new EventEmitter(false); + constructor() { + super({ enableLongStackTrace: false }); + } + run(fn: any): any { + return fn(); + } + runOutsideAngular(fn: any): any { + return fn(); + } + simulateZoneExit(): void { + this.onStable.emit(null); + } + + isStable: boolean = true; + } + + @Injectable() + class MockRouter extends Router { + readonly currentNavigationExtras = signal({}); + + readonly currentNavigation: Signal = computed(() => ({ + ...mock(), + extras: this.currentNavigationExtras(), + })); + + // eslint-disable-next-line rxjs/no-exposed-subjects + readonly routerEventsSubject = new Subject(); + + override get events() { + return this.routerEventsSubject.asObservable(); + } + } + + let service: RouterFocusManagerService; + let featureFlagSubject: BehaviorSubject; + let mockRouter: MockRouter; + let mockConfigService: Partial; + let mockNgZoneRef: MockNgZone; + + let querySelectorSpy: jest.SpyInstance; + let consoleWarnSpy: jest.SpyInstance; + + beforeEach(() => { + // Mock ConfigService + featureFlagSubject = new BehaviorSubject(true); + mockConfigService = { + getFeatureFlag$: jest.fn((flag: FeatureFlag) => { + if (flag === FeatureFlag.RouterFocusManagement) { + return featureFlagSubject.asObservable(); + } + return new BehaviorSubject(false).asObservable(); + }), + }; + + // Spy on document.querySelector and console.warn + querySelectorSpy = jest.spyOn(document, "querySelector"); + consoleWarnSpy = jest.spyOn(console, "warn").mockImplementation(); + + TestBed.configureTestingModule({ + providers: [ + RouterFocusManagerService, + { provide: Router, useClass: MockRouter }, + { provide: ConfigService, useValue: mockConfigService }, + { provide: NgZone, useClass: MockNgZone }, + { provide: DestroyRef, useValue: { onDestroy: jest.fn() } }, + ], + }); + + service = TestBed.inject(RouterFocusManagerService); + mockNgZoneRef = TestBed.inject(NgZone) as MockNgZone; + mockRouter = TestBed.inject(Router) as MockRouter; + }); + + afterEach(() => { + querySelectorSpy.mockRestore(); + consoleWarnSpy.mockRestore(); + + mockNgZoneRef.isStable = true; + TestBed.resetTestingModule(); + }); + + describe("default behavior", () => { + it("should focus main element after navigation", () => { + const mainElement = document.createElement("main"); + mainElement.focus = jest.fn(); + querySelectorSpy.mockReturnValue(mainElement); + + // Subscribe to start the service + service.start$.subscribe(); + + // Emit first navigation (should be skipped) + mockRouter.routerEventsSubject.next(new NavigationEnd(1, "/first", "/first")); + + // Emit second navigation (should trigger focus) + mockRouter.routerEventsSubject.next(new NavigationEnd(2, "/test", "/test")); + + expect(querySelectorSpy).toHaveBeenCalledWith("main"); + expect(mainElement.focus).toHaveBeenCalled(); + }); + }); + + describe("custom selector", () => { + it("should focus custom element when focusAfterNav selector is provided", () => { + const customElement = document.createElement("button"); + customElement.id = "custom-btn"; + customElement.focus = jest.fn(); + querySelectorSpy.mockReturnValue(customElement); + + // Subscribe to start the service + service.start$.subscribe(); + + // Emit first navigation (should be skipped) + mockRouter.routerEventsSubject.next(new NavigationEnd(1, "/first", "/first")); + + // Emit second navigation with custom selector + mockRouter.currentNavigationExtras.set({ state: { focusAfterNav: "#custom-btn" } }); + mockRouter.routerEventsSubject.next(new NavigationEnd(2, "/test", "/test")); + + expect(querySelectorSpy).toHaveBeenCalledWith("#custom-btn"); + expect(customElement.focus).toHaveBeenCalled(); + }); + }); + + describe("opt-out", () => { + it("should not focus when focusAfterNav is false", () => { + const mainElement = document.createElement("main"); + mainElement.focus = jest.fn(); + querySelectorSpy.mockReturnValue(mainElement); + + // Subscribe to start the service + service.start$.subscribe(); + + // Emit first navigation (should be skipped) + mockRouter.routerEventsSubject.next(new NavigationEnd(1, "/first", "/first")); + + // Emit second navigation with opt-out + mockRouter.currentNavigationExtras.set({ state: { focusAfterNav: false } }); + mockRouter.routerEventsSubject.next(new NavigationEnd(2, "/test", "/test")); + + expect(querySelectorSpy).not.toHaveBeenCalled(); + expect(mainElement.focus).not.toHaveBeenCalled(); + }); + }); + + describe("element not found", () => { + it("should log warning when custom selector does not match any element", () => { + querySelectorSpy.mockReturnValue(null); + + // Subscribe to start the service + service.start$.subscribe(); + + // Emit first navigation (should be skipped) + mockRouter.routerEventsSubject.next(new NavigationEnd(1, "/first", "/first")); + + // Emit second navigation with non-existent selector + mockRouter.currentNavigationExtras.set({ state: { focusAfterNav: "#non-existent" } }); + mockRouter.routerEventsSubject.next(new NavigationEnd(2, "/test", "/test")); + + expect(querySelectorSpy).toHaveBeenCalledWith("#non-existent"); + expect(consoleWarnSpy).toHaveBeenCalledWith( + 'RouterFocusManager: Could not find element with selector "#non-existent"', + ); + }); + }); + + // Remove describe block when FeatureFlag.RouterFocusManagement is removed + describe("feature flag", () => { + it("should not activate when RouterFocusManagement flag is disabled", () => { + const mainElement = document.createElement("main"); + mainElement.focus = jest.fn(); + querySelectorSpy.mockReturnValue(mainElement); + + // Disable feature flag + featureFlagSubject.next(false); + + // Subscribe to start the service + service.start$.subscribe(); + + // Emit first navigation (should be skipped) + mockRouter.routerEventsSubject.next(new NavigationEnd(1, "/first", "/first")); + + // Emit second navigation with flag disabled + mockRouter.routerEventsSubject.next(new NavigationEnd(2, "/test", "/test")); + + expect(querySelectorSpy).not.toHaveBeenCalled(); + expect(mainElement.focus).not.toHaveBeenCalled(); + }); + + it("should activate when RouterFocusManagement flag is enabled", () => { + const mainElement = document.createElement("main"); + mainElement.focus = jest.fn(); + querySelectorSpy.mockReturnValue(mainElement); + + // Ensure feature flag is enabled + featureFlagSubject.next(true); + + // Subscribe to start the service + service.start$.subscribe(); + + // Emit first navigation (should be skipped) + mockRouter.routerEventsSubject.next(new NavigationEnd(1, "/first", "/first")); + + // Emit second navigation with flag enabled + mockRouter.routerEventsSubject.next(new NavigationEnd(2, "/test", "/test")); + + expect(querySelectorSpy).toHaveBeenCalledWith("main"); + expect(mainElement.focus).toHaveBeenCalled(); + }); + }); + + describe("first navigation skip", () => { + it("should not trigger focus management on first navigation after page load", () => { + const mainElement = document.createElement("main"); + mainElement.focus = jest.fn(); + querySelectorSpy.mockReturnValue(mainElement); + + // Subscribe to start the service + service.start$.subscribe(); + + // Emit first navigation (should be skipped) + mockRouter.routerEventsSubject.next(new NavigationEnd(1, "/first", "/first")); + + expect(querySelectorSpy).not.toHaveBeenCalled(); + expect(mainElement.focus).not.toHaveBeenCalled(); + }); + + it("should trigger focus management on second and subsequent navigations", () => { + const mainElement = document.createElement("main"); + mainElement.focus = jest.fn(); + querySelectorSpy.mockReturnValue(mainElement); + + // Subscribe to start the service + service.start$.subscribe(); + + // Emit first navigation (should be skipped) + mockRouter.routerEventsSubject.next(new NavigationEnd(1, "/first", "/first")); + + // Emit second navigation (should trigger focus) + mockRouter.routerEventsSubject.next(new NavigationEnd(2, "/second", "/second")); + + expect(querySelectorSpy).toHaveBeenCalledWith("main"); + expect(mainElement.focus).toHaveBeenCalledTimes(1); + + // Emit third navigation (should also trigger focus) + mainElement.focus = jest.fn(); // Reset mock + mockRouter.routerEventsSubject.next(new NavigationEnd(3, "/third", "/third")); + + expect(mainElement.focus).toHaveBeenCalledTimes(1); + }); + }); + + describe("NgZone stability", () => { + it("should focus immediately when zone is stable", () => { + const mainElement = document.createElement("main"); + mainElement.focus = jest.fn(); + querySelectorSpy.mockReturnValue(mainElement); + + // Subscribe to start the service + service.start$.subscribe(); + + // Emit first navigation (should be skipped) + mockRouter.routerEventsSubject.next(new NavigationEnd(1, "/first", "/first")); + + // Emit second navigation + mockRouter.routerEventsSubject.next(new NavigationEnd(2, "/test", "/test")); + + expect(mainElement.focus).toHaveBeenCalled(); + }); + + it("should wait for zone stability before focusing when zone is not stable", async () => { + const mainElement = document.createElement("main"); + mainElement.focus = jest.fn(); + querySelectorSpy.mockReturnValue(mainElement); + + // Set zone as not stable + mockNgZoneRef.isStable = false; + + // Subscribe to start the service + service.start$.subscribe(); + + // Emit first navigation (should be skipped) + mockRouter.routerEventsSubject.next(new NavigationEnd(1, "/first", "/first")); + + // Emit second navigation + mockRouter.routerEventsSubject.next(new NavigationEnd(2, "/test", "/test")); + + // Focus should not happen yet + expect(mainElement.focus).not.toHaveBeenCalled(); + + // Emit zone stability + mockNgZoneRef.onStable.emit(true); + + // flush promises + await Promise.resolve(); + + // Now focus should have happened + expect(mainElement.focus).toHaveBeenCalled(); + }); + }); +}); diff --git a/libs/components/src/a11y/router-focus-manager.service.ts b/libs/components/src/a11y/router-focus-manager.service.ts index f7371e02a17..56e91e94e3a 100644 --- a/libs/components/src/a11y/router-focus-manager.service.ts +++ b/libs/components/src/a11y/router-focus-manager.service.ts @@ -1,40 +1,23 @@ -import { inject, Injectable } from "@angular/core"; +import { inject, Injectable, NgZone } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { NavigationEnd, Router } from "@angular/router"; -import { skip, filter, combineLatestWith, tap } from "rxjs"; +import { skip, filter, combineLatestWith, tap, map, firstValueFrom } from "rxjs"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { queryForAutofocusDescendents } from "../input"; + @Injectable({ providedIn: "root" }) export class RouterFocusManagerService { private router = inject(Router); + private ngZone = inject(NgZone); private configService = inject(ConfigService); /** - * Handles SPA route focus management. SPA apps don't automatically notify screenreader - * users that navigation has occured or move the user's focus to the content they are - * navigating to, so we need to do it. - * - * By default, we focus the `main` after an internal route navigation. - * - * Consumers can opt out of the passing the following to the `state` input. Using `state` - * allows us to access the value between browser back/forward arrows. - * In template: `` - * In typescript: `this.router.navigate([], { state: { focusMainAfterNav: false }})` - * - * Or, consumers can use the autofocus directive on an applicable interactive element. - * The autofocus directive will take precedence over this route focus pipeline. - * - * Example of where you might want to manually opt out: - * - Tab component causes a route navigation, but the tab content should be focused, - * not the whole `main` - * - * Note that router events that cause a fully new page to load (like switching between - * products) will not follow this pipeline. Instead, those will automatically bring - * focus to the top of the html document as if it were a full page load. So those links - * do not need to manually opt out of this pipeline. + * See associated router-focus-manager.mdx page for documentation on what this pipeline does and + * how to customize focus behavior. */ start$ = this.router.events.pipe( takeUntilDestroyed(), @@ -46,19 +29,47 @@ export class RouterFocusManagerService { skip(1), combineLatestWith(this.configService.getFeatureFlag$(FeatureFlag.RouterFocusManagement)), filter(([_navEvent, flagEnabled]) => flagEnabled), - filter(() => { + map(() => { const currentNavExtras = this.router.currentNavigation()?.extras; - const focusMainAfterNav: boolean | undefined = currentNavExtras?.state?.focusMainAfterNav; + const focusAfterNav: boolean | string | undefined = currentNavExtras?.state?.focusAfterNav; - return focusMainAfterNav !== false; + return focusAfterNav; }), - tap(() => { - const mainEl = document.querySelector("main"); + filter((focusAfterNav) => { + return focusAfterNav !== false; + }), + tap(async (focusAfterNav) => { + let elSelector: string = "main"; - if (mainEl) { - mainEl.focus(); + if (typeof focusAfterNav === "string" && focusAfterNav.length > 0) { + elSelector = focusAfterNav; + } + + if (this.ngZone.isStable) { + this.focusTargetEl(elSelector); + } else { + await firstValueFrom(this.ngZone.onStable); + + this.focusTargetEl(elSelector); } }), ); + + private focusTargetEl(elSelector: string) { + const targetEl = document.querySelector(elSelector); + const mainEl = document.querySelector("main"); + + const pageHasAutofocusEl = mainEl && queryForAutofocusDescendents(mainEl).length > 0; + + if (pageHasAutofocusEl) { + // do nothing because autofocus will handle the focus + return; + } else if (targetEl) { + targetEl.focus(); + } else { + // eslint-disable-next-line no-console + console.warn(`RouterFocusManager: Could not find element with selector "${elSelector}"`); + } + } } diff --git a/libs/components/src/dialog/dialog/dialog.component.ts b/libs/components/src/dialog/dialog/dialog.component.ts index c32ce176d27..39a4db88695 100644 --- a/libs/components/src/dialog/dialog/dialog.component.ts +++ b/libs/components/src/dialog/dialog/dialog.component.ts @@ -20,6 +20,7 @@ import { combineLatest, firstValueFrom, switchMap } from "rxjs"; import { I18nPipe } from "@bitwarden/ui-common"; import { BitIconButtonComponent } from "../../icon-button/icon-button.component"; +import { queryForAutofocusDescendents } from "../../input"; import { SpinnerComponent } from "../../spinner"; import { TypographyDirective } from "../../typography/typography.directive"; import { hasScrollableContent$ } from "../../utils/"; @@ -67,7 +68,7 @@ const drawerSizeToWidth = { export class DialogComponent implements AfterViewInit { private readonly destroyRef = inject(DestroyRef); private readonly ngZone = inject(NgZone); - private readonly el = inject(ElementRef); + private readonly el = inject>(ElementRef); private readonly dialogHeader = viewChild.required>("dialogHeader"); @@ -187,8 +188,7 @@ export class DialogComponent implements AfterViewInit { * AutofocusDirective. */ const dialogRef = this.el.nativeElement; - // Must match selectors of AutofocusDirective - const autofocusDescendants = dialogRef.querySelectorAll("[appAutofocus], [bitAutofocus]"); + const autofocusDescendants = queryForAutofocusDescendents(dialogRef); const hasAutofocusDescendants = autofocusDescendants.length > 0; if (!hasAutofocusDescendants) { diff --git a/libs/components/src/input/autofocus.directive.ts b/libs/components/src/input/autofocus.directive.ts index bffac8eb757..a4390405e55 100644 --- a/libs/components/src/input/autofocus.directive.ts +++ b/libs/components/src/input/autofocus.directive.ts @@ -13,6 +13,18 @@ import { Utils } from "@bitwarden/common/platform/misc/utils"; import { FocusableElement } from "../shared/focusable-element"; +/** + * Helper function to query for descendents of a given el that have the AutofocusDirective + * applied to them + * + * @param el element that supports querySelectorAll + * @returns querySelectorAll results + */ +export function queryForAutofocusDescendents(el: Document | Element) { + // ensure selectors match the directive selectors + return el.querySelectorAll("[appAutofocus], [bitAutofocus]"); +} + /** * Directive to focus an element. * @@ -21,9 +33,7 @@ import { FocusableElement } from "../shared/focusable-element"; * Will focus the element once, when it becomes visible. * * If the component provides the `FocusableElement` interface, the `focus` - * method will be called. Otherwise, the native element will be focused. - * - * If selector changes, `dialog.component.ts` must also be updated + * method will be called. Otherwise, the native element will be focused. * */ @Directive({ selector: "[appAutofocus], [bitAutofocus]", diff --git a/libs/components/src/navigation/nav-item.component.html b/libs/components/src/navigation/nav-item.component.html index 9dda3b3b6a7..a0f2d8ae38b 100644 --- a/libs/components/src/navigation/nav-item.component.html +++ b/libs/components/src/navigation/nav-item.component.html @@ -32,6 +32,9 @@ [ariaCurrentWhenActive]="ariaCurrentWhenActive()" (isActiveChange)="setIsActive($event)" (click)="mainContentClicked.emit()" + [state]="{ + focusAfterNav: focusAfterNavTarget(), + }" > diff --git a/libs/components/src/navigation/nav-item.component.ts b/libs/components/src/navigation/nav-item.component.ts index 53b181ec083..d4ef4855dd8 100644 --- a/libs/components/src/navigation/nav-item.component.ts +++ b/libs/components/src/navigation/nav-item.component.ts @@ -91,6 +91,18 @@ export class NavItemComponent extends NavBaseComponent { */ readonly ariaCurrentWhenActive = input("page"); + /** + * By default, a navigation will put the user's focus on the `main` element. + * + * If the user's focus should be moved to another element upon navigation end, pass a selector + * here (i.e. `#elementId`). + * + * Pass `false` to opt out of moving the focus entirely. Focus will stay on the nav item. + * + * See router-focus-manager.service for implementation of focus management + */ + readonly focusAfterNavTarget = input(); + /** * The design spec calls for the an outline to wrap the entire element when the template's * anchor/button has :focus-visible. Usually, we would use :focus-within for this. However, that diff --git a/libs/components/src/tabs/tab-nav-bar/tab-link.component.html b/libs/components/src/tabs/tab-nav-bar/tab-link.component.html index aa36eb37f99..932e2ce3b69 100644 --- a/libs/components/src/tabs/tab-nav-bar/tab-link.component.html +++ b/libs/components/src/tabs/tab-nav-bar/tab-link.component.html @@ -5,7 +5,7 @@ [routerLinkActiveOptions]="routerLinkMatchOptions" #rla="routerLinkActive" [active]="rla.isActive" - [state]="{ focusMainAfterNav: false }" + [state]="{ focusAfterNav: false }" [disabled]="disabled" [attr.aria-disabled]="disabled" ariaCurrentWhenActive="page" diff --git a/libs/vault/src/services/routed-vault-filter.service.ts b/libs/vault/src/services/routed-vault-filter.service.ts index 9005d507da7..e0d9f765361 100644 --- a/libs/vault/src/services/routed-vault-filter.service.ts +++ b/libs/vault/src/services/routed-vault-filter.service.ts @@ -82,7 +82,7 @@ export class RoutedVaultFilterService implements OnDestroy { }, queryParamsHandling: "merge", state: { - focusMainAfterNav: false, + focusAfterNav: false, }, }; return [commands, extras]; From bc23640176b9a79107f00dbab67475800c1a3775 Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Fri, 20 Feb 2026 16:09:05 +0100 Subject: [PATCH 45/89] [CL] Document the start and end icon attributes (#19100) --- .../components/src/button/button.component.ts | 20 +++++++++++++++++-- libs/components/src/button/button.mdx | 15 +++++++------- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/libs/components/src/button/button.component.ts b/libs/components/src/button/button.component.ts index 1055d134e53..4fcaaf6118e 100644 --- a/libs/components/src/button/button.component.ts +++ b/libs/components/src/button/button.component.ts @@ -1,4 +1,3 @@ -import { NgClass, NgTemplateOutlet } from "@angular/common"; import { input, HostBinding, @@ -72,7 +71,7 @@ const buttonStyles: Record = { selector: "button[bitButton], a[bitButton]", templateUrl: "button.component.html", providers: [{ provide: ButtonLikeAbstraction, useExisting: ButtonComponent }], - imports: [NgClass, NgTemplateOutlet, SpinnerComponent], + imports: [SpinnerComponent], hostDirectives: [AriaDisableDirective], }) export class ButtonComponent implements ButtonLikeAbstraction { @@ -124,14 +123,31 @@ export class ButtonComponent implements ButtonLikeAbstraction { return this.showLoadingStyle() || (this.disabledAttr() && this.loading() === false); }); + /** + * Style variant of the button. + */ readonly buttonType = input("secondary"); + /** + * Bitwarden icon displayed **before** the button label. + * Spacing between the icon and label is handled automatically. + */ readonly startIcon = input(undefined); + /** + * Bitwarden icon (`bwi-*`) displayed **after** the button label. + * Spacing between the label and icon is handled automatically. + */ readonly endIcon = input(undefined); + /** + * Size variant of the button. + */ readonly size = input("default"); + /** + * When `true`, the button expands to fill the full width of its container. + */ readonly block = input(false, { transform: booleanAttribute }); readonly loading = model(false); diff --git a/libs/components/src/button/button.mdx b/libs/components/src/button/button.mdx index 3080f6ffe4a..c74efcc58a2 100644 --- a/libs/components/src/button/button.mdx +++ b/libs/components/src/button/button.mdx @@ -80,21 +80,20 @@ where the width is fixed and the text wraps to 2 lines if exceeding the button ## With Icon -To ensure consistent icon spacing, the icon should have .5rem spacing on left or right(depending on -placement). +Use the `startIcon` and `endIcon` inputs to add a Bitwarden icon (`bwi-*`) before or after the +button label. Do not use a `` component inside the button as this may not have the correct +styling and spacing. -> NOTE: Use logical css properties to ensure LTR/RTL support. - -**If icon is placed before button label** +### Icon before the label ```html - + ``` -**If icon is placed after button label** +### Icon after the label ```html - + ``` From 1f69b96ed61d5ea770fe28f928ba80b43b066e39 Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Fri, 20 Feb 2026 16:54:36 +0100 Subject: [PATCH 46/89] Add linting rule to detect when icons are used in buttons (#19104) * Add linting rule to detect when icons are used in buttons * Update docs for links * Add lint for link --- eslint.config.mjs | 1 + libs/components/src/link/link.mdx | 32 ++++-- libs/eslint/components/index.mjs | 2 + .../no-icon-children-in-bit-button.mjs | 74 ++++++++++++++ .../no-icon-children-in-bit-button.spec.mjs | 97 +++++++++++++++++++ 5 files changed, 199 insertions(+), 7 deletions(-) create mode 100644 libs/eslint/components/no-icon-children-in-bit-button.mjs create mode 100644 libs/eslint/components/no-icon-children-in-bit-button.spec.mjs diff --git a/eslint.config.mjs b/eslint.config.mjs index 974aaafeef6..2e35b011c73 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -208,6 +208,7 @@ export default tseslint.config( { ignoreIfHas: ["bitPasswordInputToggle"] }, ], "@bitwarden/components/no-bwi-class-usage": "warn", + "@bitwarden/components/no-icon-children-in-bit-button": "warn", }, }, diff --git a/libs/components/src/link/link.mdx b/libs/components/src/link/link.mdx index 4954effb6c0..51ec62f8787 100644 --- a/libs/components/src/link/link.mdx +++ b/libs/components/src/link/link.mdx @@ -1,4 +1,12 @@ -import { Meta, Story, Primary, Controls, Title, Description } from "@storybook/addon-docs/blocks"; +import { + Meta, + Story, + Canvas, + Primary, + Controls, + Title, + Description, +} from "@storybook/addon-docs/blocks"; import * as stories from "./link.stories"; @@ -33,15 +41,25 @@ You can use one of the following variants by providing it as the `linkType` inpu If you want to display a link with a smaller text size, apply the `tw-text-sm` class. This will match the `body2` variant of the Typography directive. -## With icons +## With Icon -Text Links/buttons can have icons on left or the right. +Use the `startIcon` and `endIcon` inputs to add a Bitwarden icon (`bwi-*`) before or after the link +label. Do not use a `` component inside the link as this may not have the correct styling +and spacing. -To indicate a new or add action, the icon on is used on the -left. +### Icon before the label -An angle icon, , is used on the left to indicate an expand to -show/hide additional content. +```html +Add item +``` + +### Icon after the label + +```html +Next +``` + + ## Accessibility diff --git a/libs/eslint/components/index.mjs b/libs/eslint/components/index.mjs index 101fdde414c..23116dc6958 100644 --- a/libs/eslint/components/index.mjs +++ b/libs/eslint/components/index.mjs @@ -1,11 +1,13 @@ import requireLabelOnBiticonbutton from "./require-label-on-biticonbutton.mjs"; import requireThemeColorsInSvg from "./require-theme-colors-in-svg.mjs"; import noBwiClassUsage from "./no-bwi-class-usage.mjs"; +import noIconChildrenInBitButton from "./no-icon-children-in-bit-button.mjs"; export default { rules: { "require-label-on-biticonbutton": requireLabelOnBiticonbutton, "require-theme-colors-in-svg": requireThemeColorsInSvg, "no-bwi-class-usage": noBwiClassUsage, + "no-icon-children-in-bit-button": noIconChildrenInBitButton, }, }; diff --git a/libs/eslint/components/no-icon-children-in-bit-button.mjs b/libs/eslint/components/no-icon-children-in-bit-button.mjs new file mode 100644 index 00000000000..926093f2e44 --- /dev/null +++ b/libs/eslint/components/no-icon-children-in-bit-button.mjs @@ -0,0 +1,74 @@ +export const errorMessage = + 'Avoid placing icon elements ( or ) inside a bitButton or bitLink. ' + + "Use the [startIcon] or [endIcon] inputs instead. " + + 'Example: '; + +export default { + meta: { + type: "suggestion", + docs: { + description: + "Discourage using icon child elements inside bitButton; use startIcon/endIcon inputs instead", + category: "Best Practices", + recommended: true, + }, + schema: [], + }, + create(context) { + return { + Element(node) { + if (node.name !== "button" && node.name !== "a") { + return; + } + + const allAttrNames = [ + ...(node.attributes?.map((attr) => attr.name) ?? []), + ...(node.inputs?.map((input) => input.name) ?? []), + ]; + + if (!allAttrNames.includes("bitButton") && !allAttrNames.includes("bitLink")) { + return; + } + + for (const child of node.children ?? []) { + if (!child.name) { + continue; + } + + // child + if (child.name === "bit-icon") { + context.report({ + node: child, + message: errorMessage, + }); + continue; + } + + // child with bwi class + if (child.name === "i") { + const classAttrs = [ + ...(child.attributes?.filter((attr) => attr.name === "class") ?? []), + ...(child.inputs?.filter((input) => input.name === "class") ?? []), + ]; + + for (const classAttr of classAttrs) { + const classValue = classAttr.value || ""; + + if (typeof classValue !== "string") { + continue; + } + + if (/\bbwi\b/.test(classValue)) { + context.report({ + node: child, + message: errorMessage, + }); + break; + } + } + } + } + }, + }; + }, +}; diff --git a/libs/eslint/components/no-icon-children-in-bit-button.spec.mjs b/libs/eslint/components/no-icon-children-in-bit-button.spec.mjs new file mode 100644 index 00000000000..656a320678d --- /dev/null +++ b/libs/eslint/components/no-icon-children-in-bit-button.spec.mjs @@ -0,0 +1,97 @@ +import { RuleTester } from "@typescript-eslint/rule-tester"; + +import rule, { errorMessage } from "./no-icon-children-in-bit-button.mjs"; + +const ruleTester = new RuleTester({ + languageOptions: { + parser: require("@angular-eslint/template-parser"), + }, +}); + +ruleTester.run("no-icon-children-in-bit-button", rule.default, { + valid: [ + { + name: "should allow bitButton with startIcon input", + code: ``, + }, + { + name: "should allow bitButton with endIcon input", + code: ``, + }, + { + name: "should allow a[bitButton] with startIcon input", + code: `Link`, + }, + { + name: "should allow with bwi inside a regular button (no bitButton)", + code: ``, + }, + { + name: "should allow inside a regular div", + code: `
`, + }, + { + name: "should allow bitButton with only text content", + code: ``, + }, + { + name: "should allow without bwi class inside bitButton", + code: ``, + }, + { + name: "should allow bitLink with startIcon input", + code: `Link`, + }, + { + name: "should allow bitLink with only text content", + code: `Link`, + }, + ], + invalid: [ + { + name: "should warn on with bwi class inside button[bitButton]", + code: ``, + errors: [{ message: errorMessage }], + }, + { + name: "should warn on with bwi class and extra classes inside button[bitButton]", + code: ``, + errors: [{ message: errorMessage }], + }, + { + name: "should warn on with bwi class inside a[bitButton]", + code: ` Link`, + errors: [{ message: errorMessage }], + }, + { + name: "should warn on inside button[bitButton]", + code: ``, + errors: [{ message: errorMessage }], + }, + { + name: "should warn on inside a[bitButton]", + code: ` Copy`, + errors: [{ message: errorMessage }], + }, + { + name: "should warn on multiple icon children inside bitButton", + code: ``, + errors: [{ message: errorMessage }, { message: errorMessage }], + }, + { + name: "should warn on both and children", + code: ``, + errors: [{ message: errorMessage }, { message: errorMessage }], + }, + { + name: "should warn on with bwi class inside a[bitLink]", + code: ` Link`, + errors: [{ message: errorMessage }], + }, + { + name: "should warn on inside button[bitLink]", + code: ``, + errors: [{ message: errorMessage }], + }, + ], +}); From e82669b99969bbdbc0c815e530043d8ca79ab8d6 Mon Sep 17 00:00:00 2001 From: "bw-ghapp[bot]" <178206702+bw-ghapp[bot]@users.noreply.github.com> Date: Fri, 20 Feb 2026 17:08:39 +0100 Subject: [PATCH 47/89] Autosync the updated translations (#19095) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/web/src/locales/af/messages.json | 101 +++++++--- apps/web/src/locales/ar/messages.json | 101 +++++++--- apps/web/src/locales/az/messages.json | 101 +++++++--- apps/web/src/locales/be/messages.json | 101 +++++++--- apps/web/src/locales/bg/messages.json | 101 +++++++--- apps/web/src/locales/bn/messages.json | 101 +++++++--- apps/web/src/locales/bs/messages.json | 101 +++++++--- apps/web/src/locales/ca/messages.json | 101 +++++++--- apps/web/src/locales/cs/messages.json | 103 +++++++--- apps/web/src/locales/cy/messages.json | 101 +++++++--- apps/web/src/locales/da/messages.json | 101 +++++++--- apps/web/src/locales/de/messages.json | 109 ++++++++--- apps/web/src/locales/el/messages.json | 101 +++++++--- apps/web/src/locales/en_GB/messages.json | 101 +++++++--- apps/web/src/locales/en_IN/messages.json | 101 +++++++--- apps/web/src/locales/eo/messages.json | 101 +++++++--- apps/web/src/locales/es/messages.json | 101 +++++++--- apps/web/src/locales/et/messages.json | 101 +++++++--- apps/web/src/locales/eu/messages.json | 101 +++++++--- apps/web/src/locales/fa/messages.json | 101 +++++++--- apps/web/src/locales/fi/messages.json | 101 +++++++--- apps/web/src/locales/fil/messages.json | 101 +++++++--- apps/web/src/locales/fr/messages.json | 101 +++++++--- apps/web/src/locales/gl/messages.json | 101 +++++++--- apps/web/src/locales/he/messages.json | 101 +++++++--- apps/web/src/locales/hi/messages.json | 101 +++++++--- apps/web/src/locales/hr/messages.json | 101 +++++++--- apps/web/src/locales/hu/messages.json | 101 +++++++--- apps/web/src/locales/id/messages.json | 101 +++++++--- apps/web/src/locales/it/messages.json | 101 +++++++--- apps/web/src/locales/ja/messages.json | 101 +++++++--- apps/web/src/locales/ka/messages.json | 101 +++++++--- apps/web/src/locales/km/messages.json | 101 +++++++--- apps/web/src/locales/kn/messages.json | 101 +++++++--- apps/web/src/locales/ko/messages.json | 101 +++++++--- apps/web/src/locales/lv/messages.json | 113 ++++++++--- apps/web/src/locales/ml/messages.json | 101 +++++++--- apps/web/src/locales/mr/messages.json | 101 +++++++--- apps/web/src/locales/my/messages.json | 101 +++++++--- apps/web/src/locales/nb/messages.json | 101 +++++++--- apps/web/src/locales/ne/messages.json | 101 +++++++--- apps/web/src/locales/nl/messages.json | 101 +++++++--- apps/web/src/locales/nn/messages.json | 101 +++++++--- apps/web/src/locales/or/messages.json | 101 +++++++--- apps/web/src/locales/pl/messages.json | 101 +++++++--- apps/web/src/locales/pt_BR/messages.json | 229 ++++++++++++++--------- apps/web/src/locales/pt_PT/messages.json | 101 +++++++--- apps/web/src/locales/ro/messages.json | 101 +++++++--- apps/web/src/locales/ru/messages.json | 101 +++++++--- apps/web/src/locales/si/messages.json | 101 +++++++--- apps/web/src/locales/sk/messages.json | 101 +++++++--- apps/web/src/locales/sl/messages.json | 101 +++++++--- apps/web/src/locales/sr_CS/messages.json | 101 +++++++--- apps/web/src/locales/sr_CY/messages.json | 101 +++++++--- apps/web/src/locales/sv/messages.json | 101 +++++++--- apps/web/src/locales/ta/messages.json | 101 +++++++--- apps/web/src/locales/te/messages.json | 101 +++++++--- apps/web/src/locales/th/messages.json | 101 +++++++--- apps/web/src/locales/tr/messages.json | 101 +++++++--- apps/web/src/locales/uk/messages.json | 101 +++++++--- apps/web/src/locales/vi/messages.json | 101 +++++++--- apps/web/src/locales/zh_CN/messages.json | 113 ++++++++--- apps/web/src/locales/zh_TW/messages.json | 111 ++++++++--- 63 files changed, 4937 insertions(+), 1598 deletions(-) diff --git a/apps/web/src/locales/af/messages.json b/apps/web/src/locales/af/messages.json index eb983fc3512..72666452d86 100644 --- a/apps/web/src/locales/af/messages.json +++ b/apps/web/src/locales/af/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Gebruiker $ID$ gewysig.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5849,10 +5855,6 @@ "message": "Don't know the password? Ask the sender for the password needed to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Hierdie send is by verstek versteek. U kan sy sigbaarheid wissel deur die knop hier onder.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Laai aanhegsels af" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Persoonlike eienaarskap" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Ongeldige bevestigingskode" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/ar/messages.json b/apps/web/src/locales/ar/messages.json index 647c4602c17..67fb72af9f9 100644 --- a/apps/web/src/locales/ar/messages.json +++ b/apps/web/src/locales/ar/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Edited user $ID$.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5849,10 +5855,6 @@ "message": "لا تعرف كلمة المرور؟ اطلب من المرسل كلمة المرور المطلوبة للوصول إلى هذا الإرسال.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "هذا الإرسال مخفي بشكل افتراضي. يمكنك تبديل الرؤية باستخدام الزر أدناه.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "تحميل المرفقات" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Remove individual vault" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "رمز التحقق غير صالح" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/az/messages.json b/apps/web/src/locales/az/messages.json index fec06600593..a97c11ea852 100644 --- a/apps/web/src/locales/az/messages.json +++ b/apps/web/src/locales/az/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "$ID$ istifadəçisinə düzəliş edildi.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Access Intelligence-niz yaradılır..." }, - "fetchingMemberData": { - "message": "Üzv veriləri alınır..." - }, - "analyzingPasswordHealth": { - "message": "Parol sağlamlığı analiz edirilir..." - }, - "calculatingRiskScores": { - "message": "Risk xalı hesablanır..." - }, - "generatingReportData": { - "message": "Hesabat veriləri yaradılır..." - }, - "savingReport": { - "message": "Hesabat saxlanılır..." - }, - "compilingInsights": { - "message": "Təhlillər şərh edilir..." - }, "loadingProgress": { "message": "İrəliləyiş yüklənir" }, - "thisMightTakeFewMinutes": { - "message": "Bu, bir neçə dəqiqə çəkə bilər." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Hesabatı işə sal" @@ -5849,10 +5855,6 @@ "message": "Parolu bilmirsiniz? Bu \"Send\"ə erişmək üçün parolu göndərən şəxsdən istəyin.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Bu \"send\" ilkin olaraq gizlidir. Aşağıdakı düyməni istifadə edərək görünməni dəyişdirə bilərsiniz.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Qoşmaları endir" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "Bu riskləri və siyasət güncəlləmələrini qəbul edirəm" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Fərdi seyfi xaric et" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Yararsız doğrulama kodu" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domeni" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Birdən çox e-poçtu daxil edərkən vergül istifadə edin." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Yararsız Send parolu" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Şəxslər, Send-ə baxması üçün parolu daxil etməli olacaqlar", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "Ödəniş üsulunuzu güncəlləyərkən xəta baş verdi." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/be/messages.json b/apps/web/src/locales/be/messages.json index 4d6fe96c6d2..91fce4817d0 100644 --- a/apps/web/src/locales/be/messages.json +++ b/apps/web/src/locales/be/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Карыстальнік $ID$ адрэдагаваны.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5849,10 +5855,6 @@ "message": "Не ведаеце пароль? Спытайце ў адпраўніка пароль, які неабходны для доступу да гэтага Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Прадвызначана гэты Send схаваны. Вы можаце змяніць яго бачнасць выкарыстоўваючы кнопку ніжэй.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Download attachments" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Выдаліць асабістае сховішча" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Памылковы праверачны код" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/bg/messages.json b/apps/web/src/locales/bg/messages.json index 7b5ee07efe1..484e6fd5a1b 100644 --- a/apps/web/src/locales/bg/messages.json +++ b/apps/web/src/locales/bg/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Потребител № $ID$ е редактиран.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Създаване на Вашия анализ на достъпа…" }, - "fetchingMemberData": { - "message": "Извличане на данните за членовете…" - }, - "analyzingPasswordHealth": { - "message": "Анализиране на състоянието на паролите…" - }, - "calculatingRiskScores": { - "message": "Изчисляване на оценките на риска…" - }, - "generatingReportData": { - "message": "Създаване на данните за доклада…" - }, - "savingReport": { - "message": "Запазване на доклада…" - }, - "compilingInsights": { - "message": "Събиране на подробности…" - }, "loadingProgress": { "message": "Зареждане на напредъка" }, - "thisMightTakeFewMinutes": { - "message": "Това може да отнеме няколко минути." + "reviewingMemberData": { + "message": "Преглеждане на данните за членовете…" + }, + "analyzingPasswords": { + "message": "Анализиране на паролите…" + }, + "calculatingRisks": { + "message": "Изчисляване на рисковете…" + }, + "generatingReports": { + "message": "Създаване на доклади…" + }, + "compilingInsightsProgress": { + "message": "Събиране на подробности…" + }, + "reportGenerationDone": { + "message": "Готово!" }, "riskInsightsRunReport": { "message": "Изпълнение на доклада" @@ -5849,10 +5855,6 @@ "message": "Ако не знаете паролата, поискайте от изпращача да ви я даде.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Стандартно изпращането е скрито. Може да промените това като натиснете бутона по-долу.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Сваляне на прикачените файлове" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "Приемам тези рискове и промени в политиката" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Индивидуално притежание" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Грешен код за потвърждаване" }, + "invalidEmailOrVerificationCode": { + "message": "Грешна е-поща или код за потвърждаване" + }, "keyConnectorDomain": { "message": "Домейн на конектора за ключове" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Можете да въведете повече е-пощи, като ги разделите със запетая." }, + "emailsRequiredChangeAccessType": { + "message": "Потвърждаването на е-пощата изисква да е наличен поне един адрес на е-поща. Ако искате да премахнете всички е-пощи, променете начина за достъп по-горе." + }, "emailPlaceholder": { "message": "потребител@bitwarden.com , потребител@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Неправилна парола за Изпращане" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Хората ще трябва да въведат паролата, за да видят това Изпращане", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "Възникна грешка при обновяването на разплащателния метод." + }, + "sendPasswordInvalidAskOwner": { + "message": "Неправилна парола. Попитайте изпращача за паролата за достъп до това Изпращане.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "Това Изпращане изтича в $TIME$ на $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/bn/messages.json b/apps/web/src/locales/bn/messages.json index 9935cc538a1..3cc5f01c689 100644 --- a/apps/web/src/locales/bn/messages.json +++ b/apps/web/src/locales/bn/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Edited user $ID$.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5849,10 +5855,6 @@ "message": "Don't know the password? Ask the sender for the password needed to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "This Send is hidden by default. You can toggle its visibility using the button below.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Download attachments" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Remove individual vault" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/bs/messages.json b/apps/web/src/locales/bs/messages.json index dda9249b383..effcfd3062b 100644 --- a/apps/web/src/locales/bs/messages.json +++ b/apps/web/src/locales/bs/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Edited user $ID$.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5849,10 +5855,6 @@ "message": "Don't know the password? Ask the sender for the password needed to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "This Send is hidden by default. You can toggle its visibility using the button below.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Download attachments" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Remove individual vault" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/ca/messages.json b/apps/web/src/locales/ca/messages.json index 642ae65228e..52a0f9cdd44 100644 --- a/apps/web/src/locales/ca/messages.json +++ b/apps/web/src/locales/ca/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "S'ha editat l'usuari $ID$.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5849,10 +5855,6 @@ "message": "No sabeu la contrasenya? Demaneu al remitent la contrasenya necessària per accedir a aquest Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Aquest Send està ocult per defecte. Podeu canviar la seua visibilitat mitjançant el botó següent.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Baixa els fitxers adjunts" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Suprimeix la caixa forta individual" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Codi de verificació no vàlid" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/cs/messages.json b/apps/web/src/locales/cs/messages.json index ae41eeebfef..300b1b583b7 100644 --- a/apps/web/src/locales/cs/messages.json +++ b/apps/web/src/locales/cs/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automaticky potvrzený uživatel $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Byl upraven uživatel $ID$.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generování Vaší přístupové inteligence..." }, - "fetchingMemberData": { - "message": "Načítání dat člena..." - }, - "analyzingPasswordHealth": { - "message": "Analyzování zdraví hesla..." - }, - "calculatingRiskScores": { - "message": "Výpočet skóre rizik..." - }, - "generatingReportData": { - "message": "Generování dat hlášení..." - }, - "savingReport": { - "message": "Ukládání hlášení..." - }, - "compilingInsights": { - "message": "Sestavování přehledů..." - }, "loadingProgress": { "message": "Průběh načítání" }, - "thisMightTakeFewMinutes": { - "message": "Může to trvat několik minut." + "reviewingMemberData": { + "message": "Přezkoumávání dat členů..." + }, + "analyzingPasswords": { + "message": "Analyzování hesel..." + }, + "calculatingRisks": { + "message": "Výpočet rizik..." + }, + "generatingReports": { + "message": "Generování zpráv..." + }, + "compilingInsightsProgress": { + "message": "Sestavování přehledů..." + }, + "reportGenerationDone": { + "message": "Hotovo!" }, "riskInsightsRunReport": { "message": "Spustit hlášení" @@ -5849,10 +5855,6 @@ "message": "Neznáte heslo? Požádejte odesílatele o heslo potřebné pro přístup k tomuto Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Tento Send je ve výchozím nastavení skrytý. Viditelnost můžete přepnout pomocí tlačítka níže.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Stáhnout přílohy" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "Přijímám tato rizika a aktualizace zásad" }, + "autoConfirmEnabledByAdmin": { + "message": "Zapnuto Automatické potvrzování uživatele" + }, + "autoConfirmDisabledByAdmin": { + "message": "Vypnuto Automatické potvrzování uživatele" + }, + "autoConfirmEnabledByPortal": { + "message": "Přidána zásada automatického potvrzování uživatele" + }, + "autoConfirmDisabledByPortal": { + "message": "Odebrána zásada automatického potvrzování uživatele" + }, + "system": { + "message": "Systém" + }, "personalOwnership": { "message": "Odebrat osobní trezor" }, @@ -6757,7 +6774,7 @@ } }, "bulkResendInvitations": { - "message": "Try sending again" + "message": "Zkusit odeslat znovu" }, "bulkRemovedMessage": { "message": "Úspěšně odebráno" @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Neplatný ověřovací kód" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Doména Key Connectoru" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Zadejte více e-mailů oddělených čárkou." }, + "emailsRequiredChangeAccessType": { + "message": "Ověření e-mailu vyžaduje alespoň jednu e-mailovou adresu. Chcete-li odebrat všechny emaily, změňte výše uvedený typ přístupu." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Neplatné heslo k Send" }, + "vaultWelcomeDialogTitle": { + "message": "Jste u nás! Vítejte v Bitwardenu" + }, + "vaultWelcomeDialogDescription": { + "message": "Uložte všechna Vaše hesla a osobní informace v trezoru Bitwarden. Provedeme Vás tady." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Zahájit prohlídku" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Přeskočit" + }, "sendPasswordHelperText": { "message": "Pro zobrazení tohoto Send budou muset jednotlivci zadat heslo", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "Při aktualizaci Vaší platební metody došlo k chybě." + }, + "sendPasswordInvalidAskOwner": { + "message": "Neplatné heslo. Požádejte odesílatele o heslo potřebné pro přístup k tomuto Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "Tento Send vyprší v $TIME$ dne $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/cy/messages.json b/apps/web/src/locales/cy/messages.json index 10639a60017..9fd6bb263ed 100644 --- a/apps/web/src/locales/cy/messages.json +++ b/apps/web/src/locales/cy/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Edited user $ID$.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5849,10 +5855,6 @@ "message": "Don't know the password? Ask the sender for the password needed to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "This Send is hidden by default. You can toggle its visibility using the button below.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Download attachments" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Remove individual vault" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/da/messages.json b/apps/web/src/locales/da/messages.json index 2943127dbb8..1f89b9f62e5 100644 --- a/apps/web/src/locales/da/messages.json +++ b/apps/web/src/locales/da/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Redigerede bruger $ID$.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5849,10 +5855,6 @@ "message": "Kender ikke adgangskoden? Bed afsenderen om adgangskoden, der kræves for at tilgå denne Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Denne Send er som standard skjult. Dens synlighed kan ændres vha. knappen nedenfor.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Download vedhæftninger" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Fjern individuel boks" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Ugyldig bekræftelseskode" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/de/messages.json b/apps/web/src/locales/de/messages.json index a89e10c8d45..e76dc238b5e 100644 --- a/apps/web/src/locales/de/messages.json +++ b/apps/web/src/locales/de/messages.json @@ -269,7 +269,7 @@ } }, "numCriticalApplicationsMarkedSuccess": { - "message": "$COUNT$ applications marked critical", + "message": "$COUNT$ Anwendungen als kritisch markiert", "placeholders": { "count": { "content": "$1", @@ -278,7 +278,7 @@ } }, "numApplicationsUnmarkedCriticalSuccess": { - "message": "$COUNT$ applications marked not critical", + "message": "$COUNT$ Anwendungen als nicht-kritisch markiert", "placeholders": { "count": { "content": "$1", @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Benutzer $ID$ bearbeitet.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Deine Access Intelligence wird generiert..." }, - "fetchingMemberData": { - "message": "Mitgliedsdaten werden abgerufen..." - }, - "analyzingPasswordHealth": { - "message": "Passwortsicherheit wird analysiert..." - }, - "calculatingRiskScores": { - "message": "Risikobewertung wird berechnet..." - }, - "generatingReportData": { - "message": "Berichtsdaten werden generiert..." - }, - "savingReport": { - "message": "Bericht wird gespeichert..." - }, - "compilingInsights": { - "message": "Analyse wird zusammengestellt..." - }, "loadingProgress": { "message": "Ladefortschritt" }, - "thisMightTakeFewMinutes": { - "message": "Dies kann einige Minuten dauern." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Passwörter werden analysiert..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Berichte werden erstellt ..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Fertig!" }, "riskInsightsRunReport": { "message": "Bericht ausführen" @@ -5849,10 +5855,6 @@ "message": "Du kennst das Passwort nicht? Frage den Absender nach dem Passwort, das für dieses Send benötigt wird.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Dieses Send ist standardmäßig ausgeblendet. Du kannst die Sichtbarkeit mit dem Button unten umschalten.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Anhänge herunterladen" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "Ich akzeptiere diese Risiken und Richtlinien-Aktualisierungen" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Persönlichen Tresor entfernen" }, @@ -6439,7 +6456,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "verifyYourEmailToViewThisSend": { - "message": "Verify your email to view this Send", + "message": "Verifiziere deine E-Mail, um dieses Send anzuzeigen", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "viewSendHiddenEmailWarning": { @@ -6687,7 +6704,7 @@ } }, "reinviteSuccessToast": { - "message": "1 invitation sent" + "message": "1 Einladung gesendet" }, "bulkReinviteSentToast": { "message": "$COUNT$ invitations sent", @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Ungültiger Verifizierungscode" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector-Domain" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Gib mehrere E-Mail-Adressen ein, indem du sie mit einem Komma trennst." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "benutzer@bitwarden.com, benutzer@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Ungültiges Send-Passwort" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Personen müssen das Passwort eingeben, um dieses Send anzusehen", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "Beim Aktualisieren deiner Zahlungsmethode ist ein Fehler aufgetreten." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/el/messages.json b/apps/web/src/locales/el/messages.json index e084687382a..9700ec80b68 100644 --- a/apps/web/src/locales/el/messages.json +++ b/apps/web/src/locales/el/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Επεξεργασία χρήστη $ID$.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5849,10 +5855,6 @@ "message": "Δεν γνωρίζετε τον κωδικό; Ζητήστε από τον αποστολέα τον κωδικό που απαιτείται για την πρόσβαση σε αυτό το Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Αυτό το send είναι κρυμμένο από προεπιλογή. Μπορείτε να αλλάξετε την ορατότητά του χρησιμοποιώντας το παρακάτω κουμπί.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Λήψη συνημμένων" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Προσωπική Ιδιοκτησία" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Μη έγκυρος κωδικός επαλήθευσης" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/en_GB/messages.json b/apps/web/src/locales/en_GB/messages.json index 78242ac88dd..3920f2d2be6 100644 --- a/apps/web/src/locales/en_GB/messages.json +++ b/apps/web/src/locales/en_GB/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Edited user $ID$.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analysing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analysing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5849,10 +5855,6 @@ "message": "Don't know the password? Ask the sender for the password needed to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "This Send is hidden by default. You can toggle its visibility using the button below.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Download attachments" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Remove individual vault" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/en_IN/messages.json b/apps/web/src/locales/en_IN/messages.json index 5d1bf31a336..f9b75b283c3 100644 --- a/apps/web/src/locales/en_IN/messages.json +++ b/apps/web/src/locales/en_IN/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Edited user $ID$.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analysing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analysing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5849,10 +5855,6 @@ "message": "Don't know the password? Ask the Sender for the password needed to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "This send is hidden by default. You can toggle its visibility using the button below.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Download attachments" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Personal Ownership" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/eo/messages.json b/apps/web/src/locales/eo/messages.json index 1c9f0473adf..4b39004d896 100644 --- a/apps/web/src/locales/eo/messages.json +++ b/apps/web/src/locales/eo/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Redaktiĝis uzanto $ID$.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5849,10 +5855,6 @@ "message": "Ĉu vi ne scias la pasvorton? Petu al la Sendinto la pasvorton bezonatan por aliri ĉi tiun Sendon.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Ĉi tiu sendado estas kaŝita defaŭlte. Vi povas ŝalti ĝian videblecon per la suba butono.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Download attachments" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Persona Posedo" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/es/messages.json b/apps/web/src/locales/es/messages.json index 7e8f62b6a3c..d3b884663dd 100644 --- a/apps/web/src/locales/es/messages.json +++ b/apps/web/src/locales/es/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Usuario $ID$ editado.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5849,10 +5855,6 @@ "message": "¿No conoce la contraseña? Pídele al remitente la contraseña necesaria para acceder a este enviar.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Este Send está oculto por defecto. Puede cambiar su visibilidad usando el botón de abajo.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Descargar archivos adjuntos" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Propiedad personal" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Código de verificación no válido" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/et/messages.json b/apps/web/src/locales/et/messages.json index e101d49d3cf..c312f096f1b 100644 --- a/apps/web/src/locales/et/messages.json +++ b/apps/web/src/locales/et/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Muutis kasutajat $ID$.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5849,10 +5855,6 @@ "message": "Sa ei tea parooli? Küsi seda konkreetse Sendi saatjalt.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "See Send on vaikeseades peidetud. Saad selle nähtavust alloleva nupu abil seadistada.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Lae manused alla" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Personaalne salvestamine" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Vale kinnituskood" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/eu/messages.json b/apps/web/src/locales/eu/messages.json index 75f55a0d0f0..aa8a65b1141 100644 --- a/apps/web/src/locales/eu/messages.json +++ b/apps/web/src/locales/eu/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "$ID$ erabiltzailea editatua.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5849,10 +5855,6 @@ "message": "Ez duzu pasahitza ezagutzen? Eskatu bidaltzaileari Send honetara sartzeko behar den pasahitza.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Send hau modu lehenetsian ezkutatuta dago. Beheko botoia sakatuz alda dezakezu ikusgarritasuna.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Download attachments" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Ezabatu kutxa gotor pertsonala" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Egiaztatze-kodea ez da baliozkoa" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/fa/messages.json b/apps/web/src/locales/fa/messages.json index d1efb2a239f..215af9c7512 100644 --- a/apps/web/src/locales/fa/messages.json +++ b/apps/web/src/locales/fa/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "کاربر $ID$ ویرایش شد.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5849,10 +5855,6 @@ "message": "کلمه عبور را نمی‌دانید؟ از فرستنده کلمه عبور لازم را برای دسترسی به این ارسال بخواهید.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "این ارسال به طور پیش‌فرض پنهان است. با استفاده از دکمه زیر می‌توانید نمایان بودن آن را تغییر دهید.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "بارگیری پیوست‌ها" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "حذف گاوصندوق شخصی" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "کد تأیید نامعتبر است" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "دامنه رابط کلید" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/fi/messages.json b/apps/web/src/locales/fi/messages.json index 4c6b3ef62d8..ff37dcbdfb1 100644 --- a/apps/web/src/locales/fi/messages.json +++ b/apps/web/src/locales/fi/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Muokkasi käyttäjää \"$ID$\".", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5849,10 +5855,6 @@ "message": "Etkö tiedä salasanaa? Pyydä lähettäjältä tämän Sendin avaukseen tarvittavaa salasanaa.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Send on oletusarvoisesti piilotettu. Voit vaihtaa sen näkyvyyttä alla olevalla painikkeella.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Lataa liitteet" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Poista yksityinen holvi" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Virheellinen todennuskoodi" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/fil/messages.json b/apps/web/src/locales/fil/messages.json index e94c5b4fea6..85e4d95320e 100644 --- a/apps/web/src/locales/fil/messages.json +++ b/apps/web/src/locales/fil/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Na-edit ang user $ID$.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5849,10 +5855,6 @@ "message": "Hindi mo ba alam ang password Itanong sa nagpadala ang password na kailangan para ma-access ang Padala na ito.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Ang Send na ito ay nakatago bilang default. Maaari mong i toggle ang visibility nito gamit ang pindutan sa ibaba.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Download attachments" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Alisin ang indibidwal na vault" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Maling verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/fr/messages.json b/apps/web/src/locales/fr/messages.json index e4183ff5692..09c5fe03d34 100644 --- a/apps/web/src/locales/fr/messages.json +++ b/apps/web/src/locales/fr/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Utilisateur $ID$ automatiquement confirmé.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Utilisateur $ID$ modifié.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Génération de votre Intelligence d'Accès..." }, - "fetchingMemberData": { - "message": "Récupération des données des membres..." - }, - "analyzingPasswordHealth": { - "message": "Analyse de la santé du mot de passe..." - }, - "calculatingRiskScores": { - "message": "Calcul des niveaux de risque..." - }, - "generatingReportData": { - "message": "Génération des données du rapport..." - }, - "savingReport": { - "message": "Enregistrement du rapport..." - }, - "compilingInsights": { - "message": "Compilation des aperçus..." - }, "loadingProgress": { "message": "Chargement de la progression" }, - "thisMightTakeFewMinutes": { - "message": "Cela peut prendre quelques minutes." + "reviewingMemberData": { + "message": "Révision des données du membre..." + }, + "analyzingPasswords": { + "message": "Analyse des mots de passe..." + }, + "calculatingRisks": { + "message": "Calcul des risques..." + }, + "generatingReports": { + "message": "Génération du rapport..." + }, + "compilingInsightsProgress": { + "message": "Compilation des observations..." + }, + "reportGenerationDone": { + "message": "Fini !" }, "riskInsightsRunReport": { "message": "Exécuter le rapport" @@ -5849,10 +5855,6 @@ "message": "Vous ne connaissez pas le mot de passe ? Demandez à l'expéditeur le mot de passe nécessaire pour accéder à ce Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Ce Send est masqué par défaut. Vous pouvez changer sa visibilité en utilisant le bouton ci-dessous.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Télécharger les pièces jointes" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "J'accepte ces risques et mises à jour de la politique de sécurité" }, + "autoConfirmEnabledByAdmin": { + "message": "Activé le paramètre de confirmation automatique de l'utilisateur" + }, + "autoConfirmDisabledByAdmin": { + "message": "Désactivé le paramètre de confirmation automatique de l'utilisateur" + }, + "autoConfirmEnabledByPortal": { + "message": "Ajout de la politique de sécurité de confirmation automatique de l'utilisateur" + }, + "autoConfirmDisabledByPortal": { + "message": "Politique de sécurité de confirmation automatique de l'utilisateur retirée" + }, + "system": { + "message": "Système" + }, "personalOwnership": { "message": "Supprimer le coffre individuel" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Code de vérification invalide" }, + "invalidEmailOrVerificationCode": { + "message": "Courriel ou code de vérification invalide" + }, "keyConnectorDomain": { "message": "Domaine Key Connector" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Entrez plusieurs courriels en les séparant avec une virgule." }, + "emailsRequiredChangeAccessType": { + "message": "La vérification de courriel requiert au moins une adresse courriel. Pour retirer tous les courriels, changez le type d'accès ci-dessus." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Mot de passe Send invalide" }, + "vaultWelcomeDialogTitle": { + "message": "Vous y êtes! Bienvenue sur Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Enregistrez tous vos mots de passe et vos informations personnelles dans votre coffre de Bitwarden. Nous vous ferons faire la visite." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Commencer la visite" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Ignorer" + }, "sendPasswordHelperText": { "message": "Les personnes devront entrer le mot de passe pour afficher ce Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "Une erreur s'est produite lors de la mise à jour de votre mode de paiement." + }, + "sendPasswordInvalidAskOwner": { + "message": "Mot de passe invalide. Demandez à l'expéditeur le mot de passe nécessaire pour accéder à ce Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "Ce Send expire à $TIME$ le $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/gl/messages.json b/apps/web/src/locales/gl/messages.json index de6c7782c6d..dccfaa04c64 100644 --- a/apps/web/src/locales/gl/messages.json +++ b/apps/web/src/locales/gl/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Edited user $ID$.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5849,10 +5855,6 @@ "message": "Don't know the password? Ask the sender for the password needed to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "This Send is hidden by default. You can toggle its visibility using the button below.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Download attachments" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Remove individual vault" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/he/messages.json b/apps/web/src/locales/he/messages.json index 8829ed90e65..1dcbb3addcf 100644 --- a/apps/web/src/locales/he/messages.json +++ b/apps/web/src/locales/he/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "משתמש שנערך $ID$.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "הרץ דוח" @@ -5849,10 +5855,6 @@ "message": "לא יודע את הסיסמה? בקש מהשולח את הסיסמה הדרושה עבור סֵנְד זה.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "סֵנְד זה מוסתר כברירת מחדל. אתה יכול לשנות את מצב הנראות שלו באמצעות הלחצן למטה.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "הורד צרופות" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "אני מסכים לסיכונים ועדכוני מדיניות אלה" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "הסר כספת אישית" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "קוד אימות שגוי" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "דומיין של Key Connector" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/hi/messages.json b/apps/web/src/locales/hi/messages.json index 41d32fb9587..3ed164386a1 100644 --- a/apps/web/src/locales/hi/messages.json +++ b/apps/web/src/locales/hi/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Edited user $ID$.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5849,10 +5855,6 @@ "message": "Don't know the password? Ask the sender for the password needed to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "This Send is hidden by default. You can toggle its visibility using the button below.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Download attachments" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Remove individual vault" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/hr/messages.json b/apps/web/src/locales/hr/messages.json index 41eac503c59..7a7135cd2b2 100644 --- a/apps/web/src/locales/hr/messages.json +++ b/apps/web/src/locales/hr/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Uređen korisnik $ID$.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generiranje tvoje pristupne inteligencije..." }, - "fetchingMemberData": { - "message": "Dohvaćanje podataka o članu…" - }, - "analyzingPasswordHealth": { - "message": "Analiziranje zdravlja lozinke…" - }, - "calculatingRiskScores": { - "message": "Izračun ocjene rizika…" - }, - "generatingReportData": { - "message": "Generiranje izvješća…" - }, - "savingReport": { - "message": "Spremanje izvještaja…" - }, - "compilingInsights": { - "message": "Sastavljanje uvida…" - }, "loadingProgress": { "message": "Učitavanje napretka" }, - "thisMightTakeFewMinutes": { - "message": "Ovo može potrajati nekoliko minuta." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Pokreni izvješće" @@ -5849,10 +5855,6 @@ "message": "Ne znaš lozinku? Upitaj pošiljatelja za lozinku za pristup ovom Sendu.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Ovaj je Send zadano skriven. Moguće mu je promijeniti vidljivost.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Preuzmi privitke" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "Prihavaćam ove rizike i ažurirana pravila" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Ukloni osobni trezor" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Nevažeći kôd za provjeru" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Domena konektora ključa" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/hu/messages.json b/apps/web/src/locales/hu/messages.json index 9b8a98e5625..f6f580b7120 100644 --- a/apps/web/src/locales/hu/messages.json +++ b/apps/web/src/locales/hu/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "$ID$ felhasználó automatikusan megerősítésre került.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "$ID$ azonosítójú felhasználó módosításra került.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Hozzáférési intelligencia generálása..." }, - "fetchingMemberData": { - "message": "Tagi adatok lekérése..." - }, - "analyzingPasswordHealth": { - "message": "A jelszó állapot elemzése..." - }, - "calculatingRiskScores": { - "message": "Kockázati pontszámok kiszámítása..." - }, - "generatingReportData": { - "message": "Jelentés adatok generálása..." - }, - "savingReport": { - "message": "Jelentés mentése..." - }, - "compilingInsights": { - "message": "Betekintések összeállítása..." - }, "loadingProgress": { "message": "Feldolgozás betöltése" }, - "thisMightTakeFewMinutes": { - "message": "Ez eltarthat pár percig." + "reviewingMemberData": { + "message": "Tagi adatok lekérése..." + }, + "analyzingPasswords": { + "message": "A jelszavak elemzése..." + }, + "calculatingRisks": { + "message": "A kockázatok kiszámítása..." + }, + "generatingReports": { + "message": "Jelentések generálása..." + }, + "compilingInsightsProgress": { + "message": "Betekintések összeállítása..." + }, + "reportGenerationDone": { + "message": "Kész!" }, "riskInsightsRunReport": { "message": "Jelentés futtatása" @@ -5849,10 +5855,6 @@ "message": "Nem ismerjük a jelszót? Kérdezzünk rá a küldőnél a Send elérésére szükséges jelszóért.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Ez a Send alapértelmezésben rejtett. Az alábbi gombbal átváltható a láthatósága.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Mellékletek letöltése" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "Elfogadom ezeket a kockázatokat és a szabályzat frissítéseit." }, + "autoConfirmEnabledByAdmin": { + "message": "Az automatikus felhasználó megerősítés beállítás bekapcsolásra került." + }, + "autoConfirmDisabledByAdmin": { + "message": "Az automatikus felhasználó megerősítés beállítás kikapcsolásra került." + }, + "autoConfirmEnabledByPortal": { + "message": "Az automatikus felhasználó megerősítés rendszabály hozzáadásra került." + }, + "autoConfirmDisabledByPortal": { + "message": "Az automatikus felhasználó megerősítés rendszabály eltávolításra került." + }, + "system": { + "message": "Rendszer" + }, "personalOwnership": { "message": "Személyes tulajdon" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Érvénytelen ellenőrző kód" }, + "invalidEmailOrVerificationCode": { + "message": "Az email cím vagy az ellenőrző kód érvénytelen." + }, "keyConnectorDomain": { "message": "Key Connector tartomány" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Írjunk be több email címet vesszővel elválasztva." }, + "emailsRequiredChangeAccessType": { + "message": "Az email cím ellenőrzéshez legalább egy email cím szükséges. Az összes email cím eltávolításához módosítsuk a fenti hozzáférési típust." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Érvénytelen a Send jelszó." }, + "vaultWelcomeDialogTitle": { + "message": "Megérkeztünk! Üdvözlet a Bitwardenben" + }, + "vaultWelcomeDialogDescription": { + "message": "Az összes jelszó és személyes adat tárolása a Bitwarden trezorban. Nézzünk körbe." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Túra elkezdése" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Kihagyás" + }, "sendPasswordHelperText": { "message": "A személyeknek meg kell adniuk a jelszót a Send elem megtekintéséhez.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "Hiba történt a fizetési mód frissítésekor." + }, + "sendPasswordInvalidAskOwner": { + "message": "A jelszó érvénytelen. Kérjük el a feladótól a Send elem eléréséhez szükséges jelszót.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "A Send elem lejár: $DATE$ - $TIME$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/id/messages.json b/apps/web/src/locales/id/messages.json index 30746054e41..fb6a4908684 100644 --- a/apps/web/src/locales/id/messages.json +++ b/apps/web/src/locales/id/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "$ID$ telah diedit.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Jalankan laporan" @@ -5849,10 +5855,6 @@ "message": "Tidak tahu sandinya? Tanyakan pengirim untuk sandi yang diperlukan untuk mengakses Kirim ini.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Pengiriman ini disembunyikan secara default. Anda dapat mengubah visibilitasnya menggunakan tombol di bawah ini.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Unduh lampiran" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "Saya menerima risiko dan pembaruan kebijakan ini" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Kepemilikan Pribadi" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Kode verifikasi tidak valid" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/it/messages.json b/apps/web/src/locales/it/messages.json index 1e89c1624f6..f53262992fe 100644 --- a/apps/web/src/locales/it/messages.json +++ b/apps/web/src/locales/it/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Utente $ID$ modificato.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generazione del tuo Access Intelligence..." }, - "fetchingMemberData": { - "message": "Recupero dei dati dei membri..." - }, - "analyzingPasswordHealth": { - "message": "Analisi della salute della password..." - }, - "calculatingRiskScores": { - "message": "Calcolo dei punteggi di rischio..." - }, - "generatingReportData": { - "message": "Generazione dati del rapporto..." - }, - "savingReport": { - "message": "Salvataggio..." - }, - "compilingInsights": { - "message": "Compilazione dei dati..." - }, "loadingProgress": { "message": "Caricamento in corso" }, - "thisMightTakeFewMinutes": { - "message": "Attendi..." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Avvia report" @@ -5849,10 +5855,6 @@ "message": "Non conosci la password? Chiedi al mittente la password necessaria per accesso a questo Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Questo Send è nascosto per impostazione predefinita. Modifica la sua visibilità usando questo pulsante.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Scarica allegati" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "Accetto questi rischi e aggiornamenti sulle politiche" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Rimuovi cassaforte individuale" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Codice di verifica non valido" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Dominio Key Connector" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Inserisci più indirizzi email separandoli con virgole." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Password del Send non valida" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/ja/messages.json b/apps/web/src/locales/ja/messages.json index d86a7dce648..2b620ed1114 100644 --- a/apps/web/src/locales/ja/messages.json +++ b/apps/web/src/locales/ja/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "ユーザー「$ID$」の編集", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5849,10 +5855,6 @@ "message": "パスワードがわかりませんか?このSendにアクセスするには送信者にパスワードをご確認ください。", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "このSendはデフォルトでは非表示になっています。下のボタンで表示・非表示が切り替え可能です。", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "添付ファイルをダウンロード" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "個別の保管庫を削除" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "認証コードが間違っています" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/ka/messages.json b/apps/web/src/locales/ka/messages.json index 915ca5d6cba..3e527d955f3 100644 --- a/apps/web/src/locales/ka/messages.json +++ b/apps/web/src/locales/ka/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Edited user $ID$.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5849,10 +5855,6 @@ "message": "Don't know the password? Ask the sender for the password needed to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "This Send is hidden by default. You can toggle its visibility using the button below.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Download attachments" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Remove individual vault" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/km/messages.json b/apps/web/src/locales/km/messages.json index bc0e3b74cb6..d615154225d 100644 --- a/apps/web/src/locales/km/messages.json +++ b/apps/web/src/locales/km/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Edited user $ID$.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5849,10 +5855,6 @@ "message": "Don't know the password? Ask the sender for the password needed to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "This Send is hidden by default. You can toggle its visibility using the button below.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Download attachments" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Remove individual vault" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/kn/messages.json b/apps/web/src/locales/kn/messages.json index bd77a975193..51ed6353e01 100644 --- a/apps/web/src/locales/kn/messages.json +++ b/apps/web/src/locales/kn/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "ತಿದ್ಸಿದ ಬಳಕೆದಾರ $ID$.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5849,10 +5855,6 @@ "message": "ಪಾಸ್ವರ್ಡ್ ತಿಳಿದಿಲ್ಲವೇ? ಈ ಕಳುಹಿಸುವಿಕೆಯನ್ನು ಪ್ರವೇಶಿಸಲು ಅಗತ್ಯವಿರುವ ಪಾಸ್‌ವರ್ಡ್‌ಗಾಗಿ ಕಳುಹಿಸುವವರನ್ನು ಕೇಳಿ.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "ಈ ಕಳುಹಿಸುವಿಕೆಯನ್ನು ಪೂರ್ವನಿಯೋಜಿತವಾಗಿ ಮರೆಮಾಡಲಾಗಿದೆ. ಕೆಳಗಿನ ಬಟನ್ ಬಳಸಿ ನೀವು ಅದರ ಗೋಚರತೆಯನ್ನು ಟಾಗಲ್ ಮಾಡಬಹುದು.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Download attachments" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "ವೈಯಕ್ತಿಕ ಮಾಲೀಕತ್ವ" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/ko/messages.json b/apps/web/src/locales/ko/messages.json index 0af228dd821..a07134231bb 100644 --- a/apps/web/src/locales/ko/messages.json +++ b/apps/web/src/locales/ko/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "$ID$ 사용자를 편집했습니다.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5849,10 +5855,6 @@ "message": "비밀번호를 모르시나요? 보낸 사람에게 Send에 접근할 수 있는 비밀번호를 요청하세요.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "이 Send는 기본적으로 숨겨져 있습니다. 아래의 버튼을 눌러 공개 여부를 전환할 수 있습니다.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "첨부 파일 다운로드" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "개인 소유권" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "유효하지 않은 확인 코드" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/lv/messages.json b/apps/web/src/locales/lv/messages.json index f12dda40ea3..e50b76d1a4a 100644 --- a/apps/web/src/locales/lv/messages.json +++ b/apps/web/src/locales/lv/messages.json @@ -3339,13 +3339,13 @@ "message": "Abonements tika atjaunots." }, "resubscribe": { - "message": "Resubscribe" + "message": "Atsākt abonēšanu" }, "yourSubscriptionIsExpired": { - "message": "Your subscription is expired" + "message": "Abonements ir beidzies" }, "yourSubscriptionIsCanceled": { - "message": "Your subscription is canceled" + "message": "Abonements ir atcelts" }, "cancelConfirmation": { "message": "Vai tiešām atcelt? Tiks zaudēta piekļuve visām abonementa iespējām pēc pašreizējā norēķinu laika posma beigām." @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Labots lietotājs $ID$.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Izveido informāciju par Tavu piekļuvi…" }, - "fetchingMemberData": { - "message": "Iegūst dalībnieku datus…" - }, - "analyzingPasswordHealth": { - "message": "Izvērtē paroļu veselību…" - }, - "calculatingRiskScores": { - "message": "Aprēķina risku novērtējumu…" - }, - "generatingReportData": { - "message": "Izveido atskaites datus…" - }, - "savingReport": { - "message": "Saglabā atskaiti…" - }, - "compilingInsights": { - "message": "Apkopo ieskatus…" - }, "loadingProgress": { "message": "Ielādē virzību" }, - "thisMightTakeFewMinutes": { - "message": "Tas var aizņemt dažas minūtes." + "reviewingMemberData": { + "message": "Pārskata dalībnieku datus…" + }, + "analyzingPasswords": { + "message": "Izvērtē paroles…" + }, + "calculatingRisks": { + "message": "Aprēķina riskus…" + }, + "generatingReports": { + "message": "Izveido pārskatus…" + }, + "compilingInsightsProgress": { + "message": "Apkopo ieskatus…" + }, + "reportGenerationDone": { + "message": "Gatavs." }, "riskInsightsRunReport": { "message": "Izveidot atskaiti" @@ -5849,10 +5855,6 @@ "message": "Nezini paroli? Vaicā sūtītājam paroli, kas ir nepieciešama, lai piekļūtu šim Send!", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Šis Send ir paslēpts pēc noklusējuma. Tā redzamību var pārslēgt ar zemāk esošo pogu.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Lejupielādēt pielikumus" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "Es pieņemu šos riskus un pamatnostādnes atjauninājumus" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Personīgās īpašumtiesības" }, @@ -6439,7 +6456,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "verifyYourEmailToViewThisSend": { - "message": "Verify your email to view this Send", + "message": "Jāapliecina sava e-pasta adrese, lai apskatītu šo Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "viewSendHiddenEmailWarning": { @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Nederīgs apliecinājuma kods" }, + "invalidEmailOrVerificationCode": { + "message": "Nederīga e-pasta adrese vai apliecinājuma kods" + }, "keyConnectorDomain": { "message": "Key Connector domēns" }, @@ -11910,10 +11930,10 @@ "message": "Šeit parādīsies arhivētie vienumi, un tie netiks iekļauti vispārējās meklēšanas iznākumos un automātiskās aizpildes ieteikumos." }, "itemArchiveToast": { - "message": "Item archived" + "message": "Vienums ievietots arhīvā" }, "itemUnarchivedToast": { - "message": "Item unarchived" + "message": "Vienums izņemts no arhīva" }, "bulkArchiveItems": { "message": "Vienumi tika arhivēti" @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "E-pasta apliecināšanai ir nepieciešama vismaz viena e-pasta adrese. Lai noņemtu visas e-pasta adreses, augstāk jānomaina piekļūšanas veids." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Cilvēkiem būs jāievada parole, lai apskatītu šo Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Nederīga parole. Jāvaicā nepieciešamā parole nosūtītājam, lai piekļūtu šim Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "Šī Send derīgums beigsies $DATE$ plkst. $TIME$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/ml/messages.json b/apps/web/src/locales/ml/messages.json index e68e7d25b85..d7e686fba73 100644 --- a/apps/web/src/locales/ml/messages.json +++ b/apps/web/src/locales/ml/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Edited user $ID$.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5849,10 +5855,6 @@ "message": "പാസ്‌വേഡ് അറിയില്ലേ? ഈ അയയ്‌ക്കൽ ആക്‌സസ് ചെയ്യുന്നതിന് ആവശ്യമായ പാസ്‌വേഡിനായി അയച്ചയാളോട് ചോദിക്കുക.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "ഈ Send സ്ഥിരസ്ഥിതിയായി മറച്ചിരിക്കുന്നു. ചുവടെയുള്ള ബട്ടൺ ഉപയോഗിച്ചാൽ നിങ്ങൾക്ക് അതിന്റെ ദൃശ്യപരത ടോഗിൾ ചെയ്യാൻ കഴിയും.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Download attachments" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "വ്യക്തിഗത ഉടമസ്ഥാവകാശം" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/mr/messages.json b/apps/web/src/locales/mr/messages.json index 2abd88c1169..4f030d2368d 100644 --- a/apps/web/src/locales/mr/messages.json +++ b/apps/web/src/locales/mr/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Edited user $ID$.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5849,10 +5855,6 @@ "message": "Don't know the password? Ask the sender for the password needed to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "This Send is hidden by default. You can toggle its visibility using the button below.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Download attachments" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Remove individual vault" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/my/messages.json b/apps/web/src/locales/my/messages.json index bc0e3b74cb6..d615154225d 100644 --- a/apps/web/src/locales/my/messages.json +++ b/apps/web/src/locales/my/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Edited user $ID$.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5849,10 +5855,6 @@ "message": "Don't know the password? Ask the sender for the password needed to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "This Send is hidden by default. You can toggle its visibility using the button below.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Download attachments" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Remove individual vault" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/nb/messages.json b/apps/web/src/locales/nb/messages.json index de97b70a119..103a439220e 100644 --- a/apps/web/src/locales/nb/messages.json +++ b/apps/web/src/locales/nb/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Redigerte brukeren $ID$.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5849,10 +5855,6 @@ "message": "Vet du ikke passordet? Be avsender om nødvendig tilgang til denne Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Denne Send-en er skjult som standard. Du kan veksle synlighet ved å bruke knappen nedenfor.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Download attachments" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Personlig eierskap" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Ugyldig bekreftelseskode" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/ne/messages.json b/apps/web/src/locales/ne/messages.json index 26c942795b6..dbf4643236d 100644 --- a/apps/web/src/locales/ne/messages.json +++ b/apps/web/src/locales/ne/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Edited user $ID$.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5849,10 +5855,6 @@ "message": "Don't know the password? Ask the sender for the password needed to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "This Send is hidden by default. You can toggle its visibility using the button below.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Download attachments" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Remove individual vault" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/nl/messages.json b/apps/web/src/locales/nl/messages.json index 2748009dba8..023a1b775a1 100644 --- a/apps/web/src/locales/nl/messages.json +++ b/apps/web/src/locales/nl/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatisch bevestigde gebruiker $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Gebruiker gewijzigd $ID$.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Je toegangsinlichtingen genereren..." }, - "fetchingMemberData": { - "message": "Ledengegevens ophalen..." - }, - "analyzingPasswordHealth": { - "message": "Wachtwoordgezondheid analyseren..." - }, - "calculatingRiskScores": { - "message": "Risicoscores berekenen..." - }, - "generatingReportData": { - "message": "Rapportgegevens genereren..." - }, - "savingReport": { - "message": "Rapport opslaan..." - }, - "compilingInsights": { - "message": "Inzichten compileren..." - }, "loadingProgress": { "message": "Voortgang laden" }, - "thisMightTakeFewMinutes": { - "message": "Dit kan een paar minuten duren." + "reviewingMemberData": { + "message": "Ledengegevens controleren..." + }, + "analyzingPasswords": { + "message": "Wachtwoorden analyseren..." + }, + "calculatingRisks": { + "message": "Risicoscores berekenen..." + }, + "generatingReports": { + "message": "Rapporteren genereren..." + }, + "compilingInsightsProgress": { + "message": "Inzichten compileren..." + }, + "reportGenerationDone": { + "message": "Klaar!" }, "riskInsightsRunReport": { "message": "Rapport uitvoeren" @@ -5849,10 +5855,6 @@ "message": "Weet je het wachtwoord niet? Vraag de afzender om het wachtwoord om toegang te krijgen tot deze Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Deze Send is standaard verborgen. Je kunt de zichtbaarheid ervan in- en uitschakelen met de knop hieronder.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Bijlagen downloaden" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "Ik accepteer deze risico's en beleidsupdates" }, + "autoConfirmEnabledByAdmin": { + "message": "Automatisch gebruikersbevestigingsinstelling ingeschakeld" + }, + "autoConfirmDisabledByAdmin": { + "message": "Automatisch gebruikersbevestigingsinstelling uitgeschakeld" + }, + "autoConfirmEnabledByPortal": { + "message": "Automatisch gebruikersbevestigingsbeleid toegevoegd" + }, + "autoConfirmDisabledByPortal": { + "message": "Automatisch gebruikersbevestigingsbeleid verwijderd" + }, + "system": { + "message": "Systeem" + }, "personalOwnership": { "message": "Persoonlijk eigendom" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Ongeldige verificatiecode" }, + "invalidEmailOrVerificationCode": { + "message": "Ongeldig e-mailadres verificatiecode" + }, "keyConnectorDomain": { "message": "Key Connector-domein" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Voer meerdere e-mailadressen in door te scheiden met een komma." }, + "emailsRequiredChangeAccessType": { + "message": "E-mailverificatie vereist ten minste één e-mailadres. Om alle e-mailadressen te verwijderen, moet je het toegangstype hierboven wijzigen." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Ongeldig Send-wachtwoord" }, + "vaultWelcomeDialogTitle": { + "message": "Je bent erbij! Welkom bij Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Sla al je wachtwoorden en persoonlijke informatie op in je Bitwarden-kluis. We laten je zien hoe het werkt." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Rondleiding starten" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Overslaan" + }, "sendPasswordHelperText": { "message": "Individuen moeten het wachtwoord invoeren om deze Send te bekijken", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "Er is een fout opgetreden bij het bijwerken van je betaalmethode." + }, + "sendPasswordInvalidAskOwner": { + "message": "Onjuist wachtwoord. Vraag de afzender om het wachtwoord om toegang te krijgen tot deze Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "Deze Send verloopt om $TIME$ op $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/nn/messages.json b/apps/web/src/locales/nn/messages.json index 78a638ee05a..13f608da89d 100644 --- a/apps/web/src/locales/nn/messages.json +++ b/apps/web/src/locales/nn/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Edited user $ID$.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5849,10 +5855,6 @@ "message": "Don't know the password? Ask the sender for the password needed to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "This Send is hidden by default. You can toggle its visibility using the button below.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Download attachments" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Remove individual vault" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/or/messages.json b/apps/web/src/locales/or/messages.json index bc0e3b74cb6..d615154225d 100644 --- a/apps/web/src/locales/or/messages.json +++ b/apps/web/src/locales/or/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Edited user $ID$.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5849,10 +5855,6 @@ "message": "Don't know the password? Ask the sender for the password needed to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "This Send is hidden by default. You can toggle its visibility using the button below.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Download attachments" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Remove individual vault" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/pl/messages.json b/apps/web/src/locales/pl/messages.json index 40ba151b7ad..7b5df541186 100644 --- a/apps/web/src/locales/pl/messages.json +++ b/apps/web/src/locales/pl/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Użytkownik $ID$ został zaktualizowany.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5849,10 +5855,6 @@ "message": "Nie znasz hasła? Poproś nadawcę o hasło, aby uzyskać dostęp do wysyłki.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Ta wysyłka jest domyślnie ukryta. Możesz zmienić jej widoczność za pomocą przycisku.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Pobierz załączniki" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Własność osobista" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Kod weryfikacyjny jest nieprawidłowy" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Domena Key Connector'a" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/pt_BR/messages.json b/apps/web/src/locales/pt_BR/messages.json index f78d39715cc..eb8864d4e6c 100644 --- a/apps/web/src/locales/pt_BR/messages.json +++ b/apps/web/src/locales/pt_BR/messages.json @@ -15,7 +15,7 @@ "message": "Nenhum aplicativo crítico em risco" }, "critical": { - "message": "Critical ($COUNT$)", + "message": "Críticos ($COUNT$)", "placeholders": { "count": { "content": "$1", @@ -24,7 +24,7 @@ } }, "notCritical": { - "message": "Not critical ($COUNT$)", + "message": "Não críticos ($COUNT$)", "placeholders": { "count": { "content": "$1", @@ -33,13 +33,13 @@ } }, "criticalBadge": { - "message": "Critical" + "message": "Crítico" }, "accessIntelligence": { "message": "Inteligência de acesso" }, "noApplicationsMatchTheseFilters": { - "message": "No applications match these filters" + "message": "Nenhum aplicativo corresponde aos filtros" }, "passwordRisk": { "message": "Risco de senhas" @@ -48,7 +48,7 @@ "message": "Você não tem permissão para editar este item" }, "reviewAccessIntelligence": { - "message": "Review security reports to find and fix credential risks before they escalate." + "message": "Revise os relatórios de segurança para encontrar e corrigir riscos antes que cresçam." }, "reviewAtRiskLoginsPrompt": { "message": "Revisar credenciais em risco" @@ -269,7 +269,7 @@ } }, "numCriticalApplicationsMarkedSuccess": { - "message": "$COUNT$ applications marked critical", + "message": "$COUNT$ aplicativos marcados como críticos", "placeholders": { "count": { "content": "$1", @@ -278,7 +278,7 @@ } }, "numApplicationsUnmarkedCriticalSuccess": { - "message": "$COUNT$ applications marked not critical", + "message": "$COUNT$ aplicativos marcados como não críticos", "placeholders": { "count": { "content": "$1", @@ -287,7 +287,7 @@ } }, "markAppCountAsCritical": { - "message": "Mark $COUNT$ as critical", + "message": "Marcar $COUNT$ como críticos", "placeholders": { "count": { "content": "$1", @@ -296,7 +296,7 @@ } }, "markAppCountAsNotCritical": { - "message": "Mark $COUNT$ as not critical", + "message": "Marcar $COUNT$ como não críticos", "placeholders": { "count": { "content": "$1", @@ -311,7 +311,7 @@ "message": "Aplicativo" }, "applications": { - "message": "Applications" + "message": "Aplicativos" }, "atRiskPasswords": { "message": "Senhas em risco" @@ -650,7 +650,7 @@ "message": "E-mail" }, "emails": { - "message": "Emails" + "message": "E-mails" }, "phone": { "message": "Telefone" @@ -1284,7 +1284,7 @@ "message": "Selecionar tudo" }, "deselectAll": { - "message": "Deselect all" + "message": "Desselecionar tudo" }, "unselectAll": { "message": "Deselecionar tudo" @@ -1435,10 +1435,10 @@ "message": "Não" }, "noAuth": { - "message": "Anyone with the link" + "message": "Qualquer pessoa com o link" }, "anyOneWithPassword": { - "message": "Anyone with a password set by you" + "message": "Qualquer pessoa com uma senha configurada por você" }, "location": { "message": "Localização" @@ -3339,13 +3339,13 @@ "message": "A assinatura foi restabelecida." }, "resubscribe": { - "message": "Resubscribe" + "message": "Reinscrever-se" }, "yourSubscriptionIsExpired": { - "message": "Your subscription is expired" + "message": "Sua assinatura expirou" }, "yourSubscriptionIsCanceled": { - "message": "Your subscription is canceled" + "message": "Sua assinatura foi cancelada" }, "cancelConfirmation": { "message": "Você tem certeza que deseja cancelar? Você perderá o acesso a todos os recursos dessa assinatura no final deste ciclo de faturamento." @@ -3366,7 +3366,7 @@ "message": "Próxima cobrança" }, "nextChargeDate": { - "message": "Next charge date" + "message": "Próxima cobrança" }, "plan": { "message": "Plano" @@ -3857,7 +3857,7 @@ "message": "Editar conjunto" }, "viewCollection": { - "message": "View collection" + "message": "Ver conjunto" }, "collectionInfo": { "message": "Informações do conjunto" @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Usuário $ID$ foi confirmado automaticamente.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Editou o usuário $ID$.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Gerando sua Inteligência de Acesso..." }, - "fetchingMemberData": { - "message": "Buscando dados de membros..." - }, - "analyzingPasswordHealth": { - "message": "Analisando saúde das senhas..." - }, - "calculatingRiskScores": { - "message": "Calculando pontuações de risco..." - }, - "generatingReportData": { - "message": "Gerando dados do relatório..." - }, - "savingReport": { - "message": "Salvando relatório..." - }, - "compilingInsights": { - "message": "Compilando conhecimentos..." - }, "loadingProgress": { "message": "Progresso de carregamento" }, - "thisMightTakeFewMinutes": { - "message": "Isto pode levar alguns minutos." + "reviewingMemberData": { + "message": "Revisando os dados dos membros..." + }, + "analyzingPasswords": { + "message": "Analisando as senhas..." + }, + "calculatingRisks": { + "message": "Calculando os riscos..." + }, + "generatingReports": { + "message": "Gerando os relatórios..." + }, + "compilingInsightsProgress": { + "message": "Compilando conhecimentos..." + }, + "reportGenerationDone": { + "message": "Pronto!" }, "riskInsightsRunReport": { "message": "Executar relatório" @@ -5440,7 +5446,7 @@ "message": "Número mínimo de palavras" }, "passwordTypePolicyOverride": { - "message": "Password type", + "message": "Tipo da senha", "description": "Name of the password generator policy that overrides the user's password/passphrase selection." }, "userPreference": { @@ -5723,7 +5729,7 @@ } }, "sendCreatedDescriptionPassword": { - "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "message": "Copie e compartilhe este link do Send. O Send ficará disponível para qualquer um com o link e a senha por $TIME$.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", "placeholders": { "time": { @@ -5733,7 +5739,7 @@ } }, "sendCreatedDescriptionEmail": { - "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "message": "Copie e compartilhe este link do Send. Ele pode ser visto pelas pessoas que você especificou pelos próximos $TIME$.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", "placeholders": { "time": { @@ -5849,10 +5855,6 @@ "message": "Não sabe a senha? Peça ao remetente a senha necessária para acessar esse Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Este Send é oculto por padrão. Você pode alternar a visibilidade usando o botão abaixo.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Baixar anexos" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "Eu aceito estes riscos e atualizações de política" }, + "autoConfirmEnabledByAdmin": { + "message": "Ativou a configuração de confirmação de usuários automática" + }, + "autoConfirmDisabledByAdmin": { + "message": "Desativou a configuração de confirmação de usuários automática" + }, + "autoConfirmEnabledByPortal": { + "message": "Adicionou a política de confirmação de usuários automática" + }, + "autoConfirmDisabledByPortal": { + "message": "Removeu a política de confirmação de usuários automática" + }, + "system": { + "message": "Sistema" + }, "personalOwnership": { "message": "Remover cofre individual" }, @@ -6439,7 +6456,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "verifyYourEmailToViewThisSend": { - "message": "Verify your email to view this Send", + "message": "Confirme seu e-mail para ver este Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "viewSendHiddenEmailWarning": { @@ -6687,10 +6704,10 @@ } }, "reinviteSuccessToast": { - "message": "1 invitation sent" + "message": "1 convite enviado" }, "bulkReinviteSentToast": { - "message": "$COUNT$ invitations sent", + "message": "$COUNT$ convites enviados", "placeholders": { "count": { "content": "$1", @@ -6716,7 +6733,7 @@ } }, "bulkReinviteProgressTitle": { - "message": "$COUNT$ of $TOTAL$ invitations sent...", + "message": "$COUNT$ dos $TOTAL$ convites foram enviados...", "placeholders": { "count": { "content": "$1", @@ -6729,10 +6746,10 @@ } }, "bulkReinviteProgressSubtitle": { - "message": "Keep this page open until all are sent." + "message": "Mantenha esta página aberta até que todos sejam enviados." }, "bulkReinviteFailuresTitle": { - "message": "$COUNT$ invitations didn't send", + "message": "$COUNT$ convites não foram enviados", "placeholders": { "count": { "content": "$1", @@ -6741,10 +6758,10 @@ } }, "bulkReinviteFailureTitle": { - "message": "1 invitation didn't send" + "message": "1 convite não foi enviado" }, "bulkReinviteFailureDescription": { - "message": "An error occurred while sending invitations to $COUNT$ of $TOTAL$ members. Try sending again, and if the problem continues,", + "message": "Ocorreu um erro ao enviar $COUNT$ convites para os $TOTAL$ membros. Tente enviar de novo, e se o problema continuar,", "placeholders": { "count": { "content": "$1", @@ -6757,7 +6774,7 @@ } }, "bulkResendInvitations": { - "message": "Try sending again" + "message": "Tentar enviar de novo" }, "bulkRemovedMessage": { "message": "Removido com sucesso" @@ -7092,16 +7109,16 @@ "message": "Uma ou mais políticas da organização impedem que você exporte seu cofre individual." }, "activateAutofillPolicy": { - "message": "Activate autofill" + "message": "Ativar preenchimento automático" }, "activateAutofillPolicyDescription": { - "message": "Activate the autofill on page load setting on the browser extension for all existing and new members." + "message": "Ative a configuração de preenchimento automático no carregamento da página na extensão do navegador para todos os membros existentes e novos." }, "autofillOnPageLoadExploitWarning": { - "message": "Compromised or untrusted websites can exploit autofill on page load." + "message": "Sites comprometidos ou não confiáveis podem explorar do preenchimento automático ao carregar a página." }, "learnMoreAboutAutofillPolicy": { - "message": "Learn more about autofill" + "message": "Saiba mais sobre preenchimento automático" }, "selectType": { "message": "Selecionar tipo de SSO" @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Código de verificação inválido" }, + "invalidEmailOrVerificationCode": { + "message": "Email ou código de verificação inválido" + }, "keyConnectorDomain": { "message": "Domínio do Key Connector" }, @@ -10198,7 +10218,7 @@ "message": "Atribuir tarefas" }, "allTasksAssigned": { - "message": "All tasks have been assigned" + "message": "Todas as tarefas foram atribuídas" }, "assignSecurityTasksToMembers": { "message": "Envie notificações para alteração de senhas" @@ -10608,7 +10628,7 @@ "message": "Falha ao salvar a integração. Tente novamente mais tarde." }, "mustBeOrganizationOwnerAdmin": { - "message": "You must be an Organization Owner or Admin to perform this action." + "message": "Você precisa ser proprietário ou administrador da organização para executar esta ação." }, "mustBeOrgOwnerToPerformAction": { "message": "Você precisa ser o proprietário da organização para executar esta ação." @@ -11539,13 +11559,13 @@ "message": "O Bitwarden tentará reivindicar o domínio 3 vezes durante as primeiras 72 horas. Se o domínio não poder ser reivindicado, confira o registro de DNS no seu servidor e reivindique manualmente. Se não for reivindicado, o domínio será removido da sua organização em 7 dias." }, "automaticDomainClaimProcess1": { - "message": "Bitwarden will attempt to claim the domain within 72 hours. If the domain can't be claimed, verify your DNS record and claim manually. Unclaimed domains are removed after 7 days." + "message": "Bitwarden tentará reivindicar o domínio dentro de 72 horas. Se o domínio não puder ser reivindicado, verifique o seu registro DNS e reivindique manualmente. Domínios não reivindicados são removidos após 7 dias." }, "automaticDomainClaimProcess2": { - "message": "Once claimed, existing members with claimed domains will be emailed about the " + "message": "Ao reivindicar, os membros existentes com domínios reivindicados serão enviados um e-mail sobre a " }, "accountOwnershipChange": { - "message": "account ownership change" + "message": "alteração de propriedade da conta" }, "automaticDomainClaimProcessEnd": { "message": "." @@ -11563,7 +11583,7 @@ "message": "Reivindicado" }, "domainStatusPending": { - "message": "Pending" + "message": "Pendente" }, "claimedDomainsDescription": { "message": "Reivindique um domínio para ser o proprietário das contas dos membros. A página do identificador do SSO será pulada durante a autenticação dos membros com os domínios reivindicados, e os administradores poderão apagar contas reivindicadas." @@ -11910,10 +11930,10 @@ "message": "Os itens arquivados aparecerão aqui e serão excluídos dos resultados gerais de busca e das sugestões de preenchimento automático." }, "itemArchiveToast": { - "message": "Item archived" + "message": "Item arquivado" }, "itemUnarchivedToast": { - "message": "Item unarchived" + "message": "Item desarquivado" }, "bulkArchiveItems": { "message": "Itens arquivados" @@ -12593,7 +12613,7 @@ "message": "Tem certeza que deseja continuar?" }, "errorCannotDecrypt": { - "message": "Error: Cannot decrypt" + "message": "Erro: Não é possível descriptografar" }, "userVerificationFailed": { "message": "Falha na verificação do usuário." @@ -12858,16 +12878,19 @@ "message": "Você usou todos os $GB$ GB do seu armazenamento criptografado. Para continuar armazenando arquivos, adicione mais armazenamento." }, "whoCanView": { - "message": "Who can view" + "message": "Quem pode visualizar" }, "specificPeople": { - "message": "Specific people" + "message": "Pessoas específicas" }, "emailVerificationDesc": { - "message": "After sharing this Send link, individuals will need to verify their email with a code to view this Send." + "message": "Após compartilhar este link de Send, indivíduos precisarão verificar seus e-mails com um código para visualizar este Send." }, "enterMultipleEmailsSeparatedByComma": { - "message": "Enter multiple emails by separating with a comma." + "message": "Digite vários e-mails, separados com uma vírgula." + }, + "emailsRequiredChangeAccessType": { + "message": "A verificação de e-mail requer pelo menos um endereço de e-mail. Para remover todos, altere o tipo de acesso acima." }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" @@ -12876,7 +12899,7 @@ "message": "Quando você remover o armazenamento, você receberá um crédito de conta proporcional que irá automaticamente para sua próxima fatura." }, "ownerBadgeA11yDescription": { - "message": "Owner, $OWNER$, show all items owned by $OWNER$", + "message": "Proprietário, $OWNER$, mostrar todos os itens pertencentes a $OWNER$", "placeholders": { "owner": { "content": "$1", @@ -12888,35 +12911,47 @@ "message": "Você tem o Premium" }, "emailProtected": { - "message": "E-mail protegido" + "message": "Protegido por e-mail" }, "invalidSendPassword": { - "message": "Invalid Send password" + "message": "Senha do Send inválida" + }, + "vaultWelcomeDialogTitle": { + "message": "Você entrou! Boas-vindas ao Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Armazene todas as suas senhas e informações pessoais no seu cofre do Bitwarden. Vamos te dar um guia." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Começar guia" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Pular" }, "sendPasswordHelperText": { - "message": "Individuals will need to enter the password to view this Send", + "message": "Os indivíduos precisarão digitar a senha para ver este Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "perUser": { - "message": "per user" + "message": "por usuário" }, "upgradeToTeams": { - "message": "Upgrade to Teams" + "message": "Fazer upgrade para o Equipes" }, "upgradeToEnterprise": { - "message": "Upgrade to Enterprise" + "message": "Fazer upgrade para o Empresarial" }, "upgradeShareEvenMore": { - "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise" + "message": "Compartilhe ainda mais com o Famílias, ou receba segurança poderosa e confiável de senhas com o Equipes ou o Empresarial" }, "organizationUpgradeTaxInformationMessage": { - "message": "Prices exclude tax and are billed annually." + "message": "Os preços excluem os impostos e são cobrados anualmente." }, "invoicePreviewErrorMessage": { - "message": "Encountered an error while generating the invoice preview." + "message": "Foi deparado um erro ao gerar a pré-visualização da fatura." }, "planProratedMembershipInMonths": { - "message": "Prorated $PLAN$ membership ($NUMOFMONTHS$)", + "message": "Assinatura $PLAN$ rateada ($NUMOFMONTHS$)", "placeholders": { "plan": { "content": "$1", @@ -12929,16 +12964,16 @@ } }, "premiumSubscriptionCredit": { - "message": "Premium subscription credit" + "message": "Crédito da assinatura Premium" }, "enterpriseMembership": { - "message": "Enterprise membership" + "message": "Assinatura Empresarial" }, "teamsMembership": { - "message": "Teams membership" + "message": "Assinatura do Equipes" }, "plansUpdated": { - "message": "You've upgraded to $PLAN$!", + "message": "Você fez upgrade para o $PLAN$!", "placeholders": { "plan": { "content": "$1", @@ -12947,6 +12982,24 @@ } }, "paymentMethodUpdateError": { - "message": "There was an error updating your payment method." + "message": "Houve um erro ao atualizar seu método de pagamento." + }, + "sendPasswordInvalidAskOwner": { + "message": "Senha inválida. Peça ao remetente a senha necessária para acessar este Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "Este send expirá às $TIME$ em $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/pt_PT/messages.json b/apps/web/src/locales/pt_PT/messages.json index a20d884c321..eb4573b336c 100644 --- a/apps/web/src/locales/pt_PT/messages.json +++ b/apps/web/src/locales/pt_PT/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Utilizador $ID$ confirmado automaticamente.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Utilizador $ID$ editado.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "A gerar a sua Inteligência de Acesso..." }, - "fetchingMemberData": { - "message": "A obter dados dos membros..." - }, - "analyzingPasswordHealth": { - "message": "A analisar a segurança da palavra-passe..." - }, - "calculatingRiskScores": { - "message": "A calcular pontuações de risco..." - }, - "generatingReportData": { - "message": "A gerar dados do relatório..." - }, - "savingReport": { - "message": "A guardar relatório..." - }, - "compilingInsights": { - "message": "A compilar insights..." - }, "loadingProgress": { "message": "A carregar progresso" }, - "thisMightTakeFewMinutes": { - "message": "Isto pode demorar alguns minutos." + "reviewingMemberData": { + "message": "A rever os dados dos membros..." + }, + "analyzingPasswords": { + "message": "A analisar palavras-passe..." + }, + "calculatingRisks": { + "message": "A calcular riscos..." + }, + "generatingReports": { + "message": "A gerar relatórios..." + }, + "compilingInsightsProgress": { + "message": "A compilar insights..." + }, + "reportGenerationDone": { + "message": "Concluído!" }, "riskInsightsRunReport": { "message": "Executar relatório" @@ -5849,10 +5855,6 @@ "message": "Não sabe a palavra-passe? Peça ao remetente a palavra-passe necessária para aceder a este Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Este Send está oculto por defeito. Pode alternar a sua visibilidade utilizando o botão abaixo.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Transferir anexos" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "Aceito estes riscos e atualizações da política" }, + "autoConfirmEnabledByAdmin": { + "message": "Definição de confirmação automática de utilizadores ativada" + }, + "autoConfirmDisabledByAdmin": { + "message": "Definição de confirmação automática de utilizadores desativada" + }, + "autoConfirmEnabledByPortal": { + "message": "Política de confirmação automática de utilizadores adicionada" + }, + "autoConfirmDisabledByPortal": { + "message": "Política de confirmação automática de utilizadores removida" + }, + "system": { + "message": "Sistema" + }, "personalOwnership": { "message": "Remover cofre pessoal" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Código de verificação inválido" }, + "invalidEmailOrVerificationCode": { + "message": "E-mail ou código de verificação inválido" + }, "keyConnectorDomain": { "message": "Domínio do Key Connector" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Introduza vários e-mails, separados por vírgula." }, + "emailsRequiredChangeAccessType": { + "message": "A verificação por e-mail requer pelo menos um endereço de e-mail. Para remover todos os e-mails, altere o tipo de acesso acima." + }, "emailPlaceholder": { "message": "utilizador@bitwarden.com , utilizador@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Palavra-passe do Send inválida" }, + "vaultWelcomeDialogTitle": { + "message": "Entrou com sucesso! Bem-vindo ao Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Armazene todas as suas palavras-passe e informações pessoais no seu cofre Bitwarden. Vamos mostrar-lhe como funciona." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Iniciar tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Saltar" + }, "sendPasswordHelperText": { "message": "Os indivíduos terão de introduzir a palavra-passe para ver este Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "Ocorreu um erro ao atualizar o seu método de pagamento." + }, + "sendPasswordInvalidAskOwner": { + "message": "Palavra-passe inválida. Peça ao remetente a palavra-passe necessária para aceder a este Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "Este Send expira às $TIME$ de $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/ro/messages.json b/apps/web/src/locales/ro/messages.json index 6dca68fa932..f0b67e015cc 100644 --- a/apps/web/src/locales/ro/messages.json +++ b/apps/web/src/locales/ro/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Utilizatorul $ID$ a fost editat.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5849,10 +5855,6 @@ "message": "Nu știți parola? Solicitați expeditorului parola necesară pentru a accesa acest Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Acest Send este ascuns în mod implicit. Puteți comuta vizibilitatea acestuia cu butonul de mai jos.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Download attachments" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Înlăturați seiful personal" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Cod de verificare nevalid" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/ru/messages.json b/apps/web/src/locales/ru/messages.json index 13aec65ba3e..0314006ab1e 100644 --- a/apps/web/src/locales/ru/messages.json +++ b/apps/web/src/locales/ru/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Автоматически подтвержденный пользователь $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Изменен пользователь $ID$.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Ваша информация о доступе генерируется..." }, - "fetchingMemberData": { - "message": "Получение данных о пользователях..." - }, - "analyzingPasswordHealth": { - "message": "Анализ здоровья пароля..." - }, - "calculatingRiskScores": { - "message": "Расчет показателей риска..." - }, - "generatingReportData": { - "message": "Генерация данных отчета..." - }, - "savingReport": { - "message": "Сохранение отчета..." - }, - "compilingInsights": { - "message": "Компиляция информации..." - }, "loadingProgress": { "message": "Прогресс загрузки" }, - "thisMightTakeFewMinutes": { - "message": "Это может занять несколько минут." + "reviewingMemberData": { + "message": "Проверка данных пользователя..." + }, + "analyzingPasswords": { + "message": "Анализ паролей..." + }, + "calculatingRisks": { + "message": "Расчет рисков..." + }, + "generatingReports": { + "message": "Формирование отчетов..." + }, + "compilingInsightsProgress": { + "message": "Компиляция информации..." + }, + "reportGenerationDone": { + "message": "Готово!" }, "riskInsightsRunReport": { "message": "Запустить отчет" @@ -5849,10 +5855,6 @@ "message": "Не знаете пароль? Для доступа к этой Send, запросите его у отправителя.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Эта Send по умолчанию скрыта. Вы можете переключить ее видимость с помощью кнопки ниже.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Скачать вложения" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "Я принимаю эти риски и политики обновления" }, + "autoConfirmEnabledByAdmin": { + "message": "Включена настройка автоматического подтверждения пользователей" + }, + "autoConfirmDisabledByAdmin": { + "message": "Отключена настройка автоматического подтверждения пользователей" + }, + "autoConfirmEnabledByPortal": { + "message": "Добавлена политика автоматического подтверждения пользователей" + }, + "autoConfirmDisabledByPortal": { + "message": "Удалена политика автоматического подтверждения пользователей" + }, + "system": { + "message": "Система" + }, "personalOwnership": { "message": "Удалить личное хранилище" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Неверный код подтверждения" }, + "invalidEmailOrVerificationCode": { + "message": "Неверный email или код подтверждения" + }, "keyConnectorDomain": { "message": "Домен соединителя ключей" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Введите несколько email, разделяя их запятой." }, + "emailsRequiredChangeAccessType": { + "message": "Для проверки электронной почты требуется как минимум один адрес email. Чтобы удалить все адреса электронной почты, измените тип доступа выше." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Неверный пароль Send" }, + "vaultWelcomeDialogTitle": { + "message": "Вы с нами! Добро пожаловать в Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Сохраняйте все свои пароли и личную информацию в хранилище Bitwarden. Мы покажем как это работает." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Начать знакомство" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Пропустить" + }, "sendPasswordHelperText": { "message": "Пользователям необходимо будет ввести пароль для просмотра этой Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "Произошла ошибка при обновлении способа оплаты." + }, + "sendPasswordInvalidAskOwner": { + "message": "Неверный пароль. Для доступа к этой Send, запросите его у отправителя.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/si/messages.json b/apps/web/src/locales/si/messages.json index ab7c7e30566..3603a36246e 100644 --- a/apps/web/src/locales/si/messages.json +++ b/apps/web/src/locales/si/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Edited user $ID$.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5849,10 +5855,6 @@ "message": "Don't know the password? Ask the sender for the password needed to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "This Send is hidden by default. You can toggle its visibility using the button below.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Download attachments" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Remove individual vault" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/sk/messages.json b/apps/web/src/locales/sk/messages.json index b1f38615a99..79e8b40f918 100644 --- a/apps/web/src/locales/sk/messages.json +++ b/apps/web/src/locales/sk/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Používateľ $ID$ upravený.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generuje sa prehľad o prístupe..." }, - "fetchingMemberData": { - "message": "Sťahujú sa dáta o členoch..." - }, - "analyzingPasswordHealth": { - "message": "Analyzuje sa odolnosť hesiel..." - }, - "calculatingRiskScores": { - "message": "Vypočítava sa úroveň ohrozenia..." - }, - "generatingReportData": { - "message": "Generujú sa dáta reportu..." - }, - "savingReport": { - "message": "Ukladá sa report..." - }, - "compilingInsights": { - "message": "Kompiluje sa prehľad..." - }, "loadingProgress": { "message": "Priebeh načítania" }, - "thisMightTakeFewMinutes": { - "message": "Môže to trvať niekoľko minút." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Generovať report" @@ -5849,10 +5855,6 @@ "message": "Neviete heslo? Požiadajte odosielateľa o heslo potrebné k prístupu k tomuto Sendu.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Tento Send je normálne skrytý. Tlačidlom nižšie môžete prepnúť jeho viditeľnosť.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Stiahnuť prílohy" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "Akceptujem tieto riziká a aktualizácie pravidiel" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Zakázať osobný trezor" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Neplatný verifikačný kód" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Doména Key Connectora" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/sl/messages.json b/apps/web/src/locales/sl/messages.json index db65260f9b1..e5a622ca157 100644 --- a/apps/web/src/locales/sl/messages.json +++ b/apps/web/src/locales/sl/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Edited user $ID$.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5849,10 +5855,6 @@ "message": "Don't know the password? Ask the sender for the password needed to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "This Send is hidden by default. You can toggle its visibility using the button below.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Download attachments" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Remove individual vault" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/sr_CS/messages.json b/apps/web/src/locales/sr_CS/messages.json index fd18cb42a06..f869499d685 100644 --- a/apps/web/src/locales/sr_CS/messages.json +++ b/apps/web/src/locales/sr_CS/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Edited user $ID$.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5849,10 +5855,6 @@ "message": "Don't know the password? Ask the sender for the password needed to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "This Send is hidden by default. You can toggle its visibility using the button below.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Download attachments" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Remove individual vault" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/sr_CY/messages.json b/apps/web/src/locales/sr_CY/messages.json index eb6484db8df..e416df73247 100644 --- a/apps/web/src/locales/sr_CY/messages.json +++ b/apps/web/src/locales/sr_CY/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Корисник $ID$ промењен.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Преузимање података о члановима..." - }, - "analyzingPasswordHealth": { - "message": "Анализа здравља лозинки..." - }, - "calculatingRiskScores": { - "message": "Израчунавање резултата ризика..." - }, - "generatingReportData": { - "message": "Генерисање података извештаја..." - }, - "savingReport": { - "message": "Чување извештаја..." - }, - "compilingInsights": { - "message": "Састављање увида..." - }, "loadingProgress": { "message": "Учитавање напретка" }, - "thisMightTakeFewMinutes": { - "message": "Ово може потрајати неколико минута." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Покрените извештај" @@ -5849,10 +5855,6 @@ "message": "Не знате лозинку? Затражите од пошиљаоца лозинку потребну за приступ овом Слању.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Ово Слање је подразумевано скривено. Можете да пребацујете његову видљивост помоћу дугмета испод.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Преузмите прилоге" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "Прихватам ове ризике и ажурирања политика" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Лично власништво" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Неисправан верификациони код" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Домен конектора кључа" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/sv/messages.json b/apps/web/src/locales/sv/messages.json index 3cb3116b684..079dce0e3a7 100644 --- a/apps/web/src/locales/sv/messages.json +++ b/apps/web/src/locales/sv/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Redigerade användaren $ID$.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Genererar din Access Intelligence..." }, - "fetchingMemberData": { - "message": "Hämtar medlemsdata..." - }, - "analyzingPasswordHealth": { - "message": "Analyserar lösenordshälsa..." - }, - "calculatingRiskScores": { - "message": "Beräknar riskpoäng..." - }, - "generatingReportData": { - "message": "Genererar rapportdata..." - }, - "savingReport": { - "message": "Sparar rapport..." - }, - "compilingInsights": { - "message": "Sammanställer insikter..." - }, "loadingProgress": { "message": "Inläsningsförlopp" }, - "thisMightTakeFewMinutes": { - "message": "Detta kan ta några minuter." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Kör rapport" @@ -5849,10 +5855,6 @@ "message": "Vet du inte lösenordet? Fråga avsändaren om lösenordet som behövs för att komma åt denna Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Denna Send är dold som standard. Du kan växla dess synlighet med hjälp av knappen nedan.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Ladda ner bilagor" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "Jag accepterar dessa risker och policyuppdateringar" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Radera individuellt valv" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Ogiltig verifieringskod" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector-domän" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Ange flera e-postadresser genom att separera dem med kommatecken." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "användare@bitwarden.com , användare@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Ogiltigt Send-lösenord" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individer måste ange lösenordet för att visa denna Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Ogiltigt lösenord. Fråga avsändaren om lösenordet som behövs för att komma åt denna Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/ta/messages.json b/apps/web/src/locales/ta/messages.json index 3789574201c..a1eb60d67c1 100644 --- a/apps/web/src/locales/ta/messages.json +++ b/apps/web/src/locales/ta/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "பயனர் $ID$ திருத்தப்பட்டார்.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5849,10 +5855,6 @@ "message": "கடவுச்சொல் தெரியவில்லையா? இந்த Send-ஐ அணுகத் தேவையான கடவுச்சொல்லை அனுப்புநரிடம் கேட்கவும்.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "இந்த Send இயல்பாக மறைக்கப்பட்டுள்ளது. கீழே உள்ள பொத்தானைப் பயன்படுத்தி அதன் தெரிவுநிலையை மாற்றலாம்.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "இணைப்புகளைப் பதிவிறக்கவும்" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "தனிப்பட்ட வால்ட்டை அகற்று" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "தவறான சரிபார்ப்புக் குறியீடு" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "கீ கனெக்டர் டொமைன்" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/te/messages.json b/apps/web/src/locales/te/messages.json index bc0e3b74cb6..d615154225d 100644 --- a/apps/web/src/locales/te/messages.json +++ b/apps/web/src/locales/te/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Edited user $ID$.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5849,10 +5855,6 @@ "message": "Don't know the password? Ask the sender for the password needed to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "This Send is hidden by default. You can toggle its visibility using the button below.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Download attachments" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Remove individual vault" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/th/messages.json b/apps/web/src/locales/th/messages.json index eb6fa9011fc..ab53147de00 100644 --- a/apps/web/src/locales/th/messages.json +++ b/apps/web/src/locales/th/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Edited user $ID$.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5849,10 +5855,6 @@ "message": "Don't know the password? Ask the sender for the password needed to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "This Send is hidden by default. You can toggle its visibility using the button below.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Download attachments" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Remove individual vault" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/tr/messages.json b/apps/web/src/locales/tr/messages.json index 53348da8697..efd186d721a 100644 --- a/apps/web/src/locales/tr/messages.json +++ b/apps/web/src/locales/tr/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Kullanıcı düzenlendi: $ID$.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Access Intelligence’ınız oluşturuluyor..." }, - "fetchingMemberData": { - "message": "Üye verileri getiriliyor..." - }, - "analyzingPasswordHealth": { - "message": "Parola sağlığı analiz ediliyor..." - }, - "calculatingRiskScores": { - "message": "Risk puanları hesaplanıyor..." - }, - "generatingReportData": { - "message": "Rapor verileri oluşturuluyor..." - }, - "savingReport": { - "message": "Rapor kaydediliyor..." - }, - "compilingInsights": { - "message": "İçgörüler derleniyor..." - }, "loadingProgress": { "message": "Yükleme ilerlemesi" }, - "thisMightTakeFewMinutes": { - "message": "Bu işlem birkaç dakika sürebilir." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Raporu çalıştır" @@ -5849,10 +5855,6 @@ "message": "Parolayı bilmiyor musunuz? Bu Send'e erişmek için gereken parolayı dosyayı gönderen kişiye sorabilirsiniz.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Bu Send varsayılan olarak gizlidir. Aşağıdaki düğmeyi kullanarak görünürlüğünü değiştirebilirsiniz.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Ekleri indir" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "Bu riskleri ve ilke güncellemelerini kabul ediyorum" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "Sistem" + }, "personalOwnership": { "message": "Kişisel kasayı kaldır" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Geçersiz doğrulama kodu" }, + "invalidEmailOrVerificationCode": { + "message": "E-posta veya doğrulama kodu geçersiz" + }, "keyConnectorDomain": { "message": "Key Connector alan adı" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "E-posta adreslerini virgülle ayırarak yazın." }, + "emailsRequiredChangeAccessType": { + "message": "E-posta doğrulaması için en az bir e-posta adresi gerekir. Tüm e-postaları silmek için yukarıdan erişim türünü değiştirin." + }, "emailPlaceholder": { "message": "kullanici@bitwarden.com , kullanici@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Geçersiz Send parolası" }, + "vaultWelcomeDialogTitle": { + "message": "Bitwarden'a hoş geldiniz" + }, + "vaultWelcomeDialogDescription": { + "message": "Tüm parolalarınızı ve kişisel bilgilerinizi Bitwarden kasanızda saklayabilirsiniz. Size etrafı gezdirelim." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Tura başla" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Geç" + }, "sendPasswordHelperText": { "message": "Bu Send'i görmek isteyen kişilerin parola girmesi gerekecektir", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "Ödeme yönteminizi güncellerken bir hata oluştu." + }, + "sendPasswordInvalidAskOwner": { + "message": "Parola geçersiz. Bu Send'e erişmek için gereken parolayı dosyayı gönderen kişiye sorabilirsiniz.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "Bu Send'in süresi $DATE$ $TIME$ tarihinde dolacaktır", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/uk/messages.json b/apps/web/src/locales/uk/messages.json index dcb5d26aa1f..71780d2faa9 100644 --- a/apps/web/src/locales/uk/messages.json +++ b/apps/web/src/locales/uk/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Користувача $ID$ змінено.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5849,10 +5855,6 @@ "message": "Не знаєте пароль? Попросіть його у відправника для отримання доступу.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Це відправлення типово приховане. Ви можете змінити його видимість кнопкою нижче.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Завантажити вкладення" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Вилучити особисте сховище" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Недійсний код підтвердження" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Домен Key Connector" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Введіть декілька адрес е-пошти, розділяючи їх комою." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/vi/messages.json b/apps/web/src/locales/vi/messages.json index 64d0703bc07..369a87111d4 100644 --- a/apps/web/src/locales/vi/messages.json +++ b/apps/web/src/locales/vi/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Người dùng $ID$ đã được chỉnh sửa.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Đang tạo Access Intelligence của bạn..." }, - "fetchingMemberData": { - "message": "Đang lấy dữ liệu thành viên..." - }, - "analyzingPasswordHealth": { - "message": "Đang phân tích độ mạnh mật khẩu..." - }, - "calculatingRiskScores": { - "message": "Đang tính điểm rủi ro..." - }, - "generatingReportData": { - "message": "Đang tạo dữ liệu báo cáo..." - }, - "savingReport": { - "message": "Đang lưu báo cáo..." - }, - "compilingInsights": { - "message": "Đang biên soạn thông tin chi tiết..." - }, "loadingProgress": { "message": "Đang tải tiến trình" }, - "thisMightTakeFewMinutes": { - "message": "Quá trình này có thể mất vài phút." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Chạy báo cáo" @@ -5849,10 +5855,6 @@ "message": "Không biết mật khẩu? Hãy yêu cầu người gửi cung cấp mật khẩu cần thiết để truy cập vào Send này.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Send này sẽ bị ẩn theo mặc định. Bạn có thể bật/tắt tính năng này bằng cách nhấn vào nút bên dưới.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Tải xuống tập tin đính kèm" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "Tôi chấp nhận những rủi ro và cập nhật chính sách này" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Xóa kho lưu trữ riêng lẻ" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Mã xác minh không đúng" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Tên miền Key Connector" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/zh_CN/messages.json b/apps/web/src/locales/zh_CN/messages.json index 47c732a5a32..275e11a1c70 100644 --- a/apps/web/src/locales/zh_CN/messages.json +++ b/apps/web/src/locales/zh_CN/messages.json @@ -3339,13 +3339,13 @@ "message": "您的订阅已恢复。" }, "resubscribe": { - "message": "Resubscribe" + "message": "重新订阅" }, "yourSubscriptionIsExpired": { - "message": "Your subscription is expired" + "message": "您的订阅已过期" }, "yourSubscriptionIsCanceled": { - "message": "Your subscription is canceled" + "message": "您的订阅已取消" }, "cancelConfirmation": { "message": "确定要取消吗?在本次计费周期结束后,您将无法使用此订阅的所有功能。" @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "编辑了用户 $ID$。", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "正在生成 Access Intelligence..." }, - "fetchingMemberData": { - "message": "正在获取成员数据..." - }, - "analyzingPasswordHealth": { - "message": "正在分析密码健康度..." - }, - "calculatingRiskScores": { - "message": "正在计算风险评分..." - }, - "generatingReportData": { - "message": "正在生成报告数据..." - }, - "savingReport": { - "message": "正在保存报告..." - }, - "compilingInsights": { - "message": "正在编译洞察..." - }, "loadingProgress": { "message": "加载进度" }, - "thisMightTakeFewMinutes": { - "message": "这可能需要几分钟时间。" + "reviewingMemberData": { + "message": "正在审查成员数据..." + }, + "analyzingPasswords": { + "message": "正在分析密码..." + }, + "calculatingRisks": { + "message": "正在计算风险..." + }, + "generatingReports": { + "message": "正在生成报告..." + }, + "compilingInsightsProgress": { + "message": "正在编译洞察..." + }, + "reportGenerationDone": { + "message": "完成!" }, "riskInsightsRunReport": { "message": "运行报告" @@ -5849,10 +5855,6 @@ "message": "不知道密码吗?请向发送者索取访问此 Send 所需的密码。", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "此 Send 默认隐藏。您可以使用下方的按钮切换其可见性。", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "下载附件" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "我接受这些风险和策略更新" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "禁用个人密码库" }, @@ -6439,7 +6456,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "verifyYourEmailToViewThisSend": { - "message": "Verify your email to view this Send", + "message": "验证您的电子邮箱以查看此 Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "viewSendHiddenEmailWarning": { @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "无效的验证码" }, + "invalidEmailOrVerificationCode": { + "message": "无效的电子邮箱或验证码" + }, "keyConnectorDomain": { "message": "Key Connector 域名" }, @@ -11910,10 +11930,10 @@ "message": "已归档的项目将显示在此处,并将被排除在一般搜索结果和自动填充建议之外。" }, "itemArchiveToast": { - "message": "Item archived" + "message": "项目已归档" }, "itemUnarchivedToast": { - "message": "Item unarchived" + "message": "项目已取消归档" }, "bulkArchiveItems": { "message": "项目已归档" @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "输入多个电子邮箱(使用逗号分隔)。" }, + "emailsRequiredChangeAccessType": { + "message": "电子邮箱验证要求至少有一个电子邮箱地址。要移除所有电子邮箱,请更改上面的访问类型。" + }, "emailPlaceholder": { "message": "user@bitwarden.com, user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "无效的 Send 密码" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "个人需要输入密码才能查看此 Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "更新您的付款方式时出错。" + }, + "sendPasswordInvalidAskOwner": { + "message": "无效的密码。请向发送者索取访问此 Send 所需的密码。", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "此 Send 有效期至 $DATE$ $TIME$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/zh_TW/messages.json b/apps/web/src/locales/zh_TW/messages.json index c006b37d612..aedc802f241 100644 --- a/apps/web/src/locales/zh_TW/messages.json +++ b/apps/web/src/locales/zh_TW/messages.json @@ -2803,7 +2803,7 @@ "message": "用戶端 ID" }, "twoFactorDuoClientSecret": { - "message": "用戶端秘密" + "message": "用戶端機密" }, "twoFactorDuoApiHostname": { "message": "API 主機名稱" @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "已編輯使用者 $ID$。", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "正在產生您的 Access Intelligence……" }, - "fetchingMemberData": { - "message": "正在擷取成員資料…" - }, - "analyzingPasswordHealth": { - "message": "正在分析密碼安全狀況…" - }, - "calculatingRiskScores": { - "message": "正在計算風險分數…" - }, - "generatingReportData": { - "message": "正在產生報告資料..." - }, - "savingReport": { - "message": "正在儲存報告..." - }, - "compilingInsights": { - "message": "正在整理洞察結果…" - }, "loadingProgress": { "message": "載入進度中" }, - "thisMightTakeFewMinutes": { - "message": "這可能需要幾分鐘。" + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "執行報告" @@ -5849,10 +5855,6 @@ "message": "不知道密碼?請向此 Send 的寄件者索取密碼。", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "此 Send 預設為隱藏。您可使用下方的按鈕切換其可見度。", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "下載附件" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "我接受這些風險與原則更新" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "停用個人密碼庫" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "無效的驗證碼" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector 網域" }, @@ -9791,7 +9811,7 @@ } }, "secretsManagerForPlanDesc": { - "message": "提供工程與 DevOps 團隊一套在軟體開發生命週期中,管理秘密資訊的功能。" + "message": "提供工程與 DevOps 團隊一套在軟體開發生命週期中,管理機密資訊的功能。" }, "free2PersonOrganization": { "message": "免費的 2 人組織" @@ -9833,7 +9853,7 @@ "message": "訂閲機密管理員" }, "addSecretsManagerUpgradeDesc": { - "message": "將機密管理員加入您的升級方案,來維持先前方案建立的秘密資訊的存取權限。" + "message": "將機密管理員加入您的升級方案,來維持先前方案建立的機密資訊的存取權限。" }, "additionalServiceAccounts": { "message": "額外服務帳戶" @@ -10906,7 +10926,7 @@ "message": "已驗證" }, "viewSecret": { - "message": "檢視秘密" + "message": "檢視機密" }, "noClients": { "message": "沒有可列出的客戶" @@ -11910,7 +11930,7 @@ "message": "封存的項目會顯示在此處,且不會出現在一般搜尋結果或自動填入建議中。" }, "itemArchiveToast": { - "message": "Item archived" + "message": "項目已封存" }, "itemUnarchivedToast": { "message": "Item unarchived" @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "請以逗號分隔輸入多個電子郵件地址。" }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Send 密碼無效" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "對方必須輸入密碼才能檢視此 Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } From a7c74c6f7614a0f0fdddd945d330e480f93c5e30 Mon Sep 17 00:00:00 2001 From: Vijay Oommen Date: Fri, 20 Feb 2026 10:17:08 -0600 Subject: [PATCH 48/89] [PM-32372] Added testid for table and then fixed tech debt (#19066) --- .../common/base.events.component.ts | 26 +-- .../manage/events.component.html | 170 +++++++++--------- .../organizations/manage/events.component.ts | 7 +- .../providers/manage/events.component.html | 112 ++++++------ .../providers/manage/events.component.ts | 2 +- .../service-accounts-events.component.html | 82 +++++---- .../service-accounts-events.component.ts | 7 +- 7 files changed, 215 insertions(+), 191 deletions(-) diff --git a/apps/web/src/app/admin-console/common/base.events.component.ts b/apps/web/src/app/admin-console/common/base.events.component.ts index ba315dee7fb..dd1c393bc13 100644 --- a/apps/web/src/app/admin-console/common/base.events.component.ts +++ b/apps/web/src/app/admin-console/common/base.events.component.ts @@ -1,6 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { Directive, OnDestroy } from "@angular/core"; +import { Directive, OnDestroy, signal } from "@angular/core"; import { FormControl, FormGroup } from "@angular/forms"; import { ActivatedRoute } from "@angular/router"; import { combineLatest, filter, map, Observable, Subject, switchMap, takeUntil } from "rxjs"; @@ -22,9 +22,9 @@ import { EventExportService } from "../../tools/event-export"; @Directive() export abstract class BaseEventsComponent implements OnDestroy { - loading = true; - loaded = false; - events: EventView[]; + readonly loading = signal(true); + readonly loaded = signal(false); + readonly events = signal([]); dirtyDates = true; continuationToken: string; canUseSM = false; @@ -115,7 +115,7 @@ export abstract class BaseEventsComponent implements OnDestroy { return; } - this.loading = true; + this.loading.set(true); const dates = this.parseDates(); if (dates == null) { @@ -131,7 +131,7 @@ export abstract class BaseEventsComponent implements OnDestroy { } promise = null; - this.loading = false; + this.loading.set(false); }; loadEvents = async (clearExisting: boolean) => { @@ -140,7 +140,7 @@ export abstract class BaseEventsComponent implements OnDestroy { return; } - this.loading = true; + this.loading.set(true); let events: EventView[] = []; let promise: Promise; promise = this.loadAndParseEvents( @@ -153,14 +153,16 @@ export abstract class BaseEventsComponent implements OnDestroy { this.continuationToken = result.continuationToken; events = result.events; - if (!clearExisting && this.events != null && this.events.length > 0) { - this.events = this.events.concat(events); + if (!clearExisting && this.events() != null && this.events().length > 0) { + this.events.update((current) => { + return [...current, ...events]; + }); } else { - this.events = events; + this.events.set(events); } this.dirtyDates = false; - this.loading = false; + this.loading.set(false); promise = null; }; @@ -227,7 +229,7 @@ export abstract class BaseEventsComponent implements OnDestroy { private async export(start: string, end: string) { let continuationToken = this.continuationToken; - let events = [].concat(this.events); + let events = [].concat(this.events()); while (continuationToken != null) { const result = await this.loadAndParseEvents(start, end, continuationToken); diff --git a/apps/web/src/app/admin-console/organizations/manage/events.component.html b/apps/web/src/app/admin-console/organizations/manage/events.component.html index 83665a4b99e..3e76c8c879b 100644 --- a/apps/web/src/app/admin-console/organizations/manage/events.component.html +++ b/apps/web/src/app/admin-console/organizations/manage/events.component.html @@ -1,14 +1,16 @@ @let usePlaceHolderEvents = !organization?.useEvents; + - - {{ "upgrade" | i18n }} - + @if (usePlaceHolderEvents) { + + {{ "upgrade" | i18n }} + + }
@@ -61,79 +63,87 @@
- - {{ "upgradeEventLogMessage" | i18n }} - - - - {{ "loading" | i18n }} - - - @let displayedEvents = organization?.useEvents ? events : placeholderEvents; +@if (loaded() && usePlaceHolderEvents) { + + {{ "upgradeEventLogMessage" | i18n }} + +} -

{{ "noEventsInList" | i18n }}

- - - - {{ "timestamp" | i18n }} - {{ "client" | i18n }} - {{ "member" | i18n }} - {{ "event" | i18n }} - - - - - - {{ i > 4 && usePlaceHolderEvents ? "******" : (e.date | date: "medium") }} - - - {{ e.appName }} - - - {{ e.userName }} - - - - - - -
+@if (!loaded()) { + + + {{ "loading" | i18n }} + +} +@if (loaded()) { + + @let displayedEvents = organization?.useEvents ? events() : placeholderEvents; - -
-
- + @if (!displayedEvents || !displayedEvents.length) { +

{{ "noEventsInList" | i18n }}

+ } -

- {{ "upgradeEventLogTitleMessage" | i18n }} -

-

- {{ "upgradeForFullEventsMessage" | i18n }} -

- - + } + +} + +@if (loaded() && usePlaceHolderEvents) { + +
+
+ + +

+ {{ "upgradeEventLogTitleMessage" | i18n }} +

+

+ {{ "upgradeForFullEventsMessage" | i18n }} +

+ + +
-
- + +} diff --git a/apps/web/src/app/admin-console/organizations/manage/events.component.ts b/apps/web/src/app/admin-console/organizations/manage/events.component.ts index fffe1c06ab8..01d6515c486 100644 --- a/apps/web/src/app/admin-console/organizations/manage/events.component.ts +++ b/apps/web/src/app/admin-console/organizations/manage/events.component.ts @@ -1,6 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { Component, OnDestroy, OnInit } from "@angular/core"; +import { Component, OnDestroy, OnInit, ChangeDetectionStrategy } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; import { concatMap, firstValueFrom, lastValueFrom, map, of, switchMap, takeUntil, tap } from "rxjs"; @@ -47,9 +47,8 @@ const EVENT_SYSTEM_USER_TO_TRANSLATION: Record = { [EventSystemUser.BitwardenPortal]: "system", }; -// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush -// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ + changeDetection: ChangeDetectionStrategy.OnPush, templateUrl: "events.component.html", imports: [SharedModule, HeaderModule], }) @@ -168,7 +167,7 @@ export class EventsComponent extends BaseEventsComponent implements OnInit, OnDe } } await this.refreshEvents(); - this.loaded = true; + this.loaded.set(true); } protected requestEvents(startDate: string, endDate: string, continuationToken: string) { diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/events.component.html b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/events.component.html index 070505a53b2..c79b39a6feb 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/events.component.html +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/events.component.html @@ -50,56 +50,64 @@
- - - {{ "loading" | i18n }} - - -

{{ "noEventsInList" | i18n }}

- - - - {{ "timestamp" | i18n }} - {{ "device" | i18n }} - {{ "user" | i18n }} - {{ "event" | i18n }} - - - - - {{ e.date | date: "medium" }} - - - {{ e.appName }}, {{ e.ip }} - - - {{ e.userName }} - - - - - - -
+ > + {{ "loading" | i18n }} + +} +@if (loaded()) { + + @if (!events() || !events().length) { +

{{ "noEventsInList" | i18n }}

+ } + + @if (events() && events().length) { + + + + {{ "timestamp" | i18n }} + {{ "device" | i18n }} + {{ "user" | i18n }} + {{ "event" | i18n }} + + + + @for (e of events(); track i; let i = $index) { + + {{ e.date | date: "medium" }} + + + {{ e.appName }}, {{ e.ip }} + + + {{ e.userName }} + + + + } + + + } + @if (continuationToken) { + + } +
+} diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/events.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/events.component.ts index 3d00d897175..fe14e56bbaa 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/events.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/events.component.ts @@ -94,7 +94,7 @@ export class EventsComponent extends BaseEventsComponent implements OnInit { this.providerUsersUserIdMap.set(u.userId, { name: name, email: u.email }); }); await this.refreshEvents(); - this.loaded = true; + this.loaded.set(true); } protected requestEvents(startDate: string, endDate: string, continuationToken: string) { diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/event-logs/service-accounts-events.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/event-logs/service-accounts-events.component.html index a895ab058ec..d1f410abc33 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/event-logs/service-accounts-events.component.html +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/event-logs/service-accounts-events.component.html @@ -47,41 +47,47 @@
- - - {{ "loading" | i18n }} - - -

{{ "noEventsInList" | i18n }}

- - - - {{ "timestamp" | i18n }} - {{ "client" | i18n }} - {{ "event" | i18n }} - - - - - {{ e.date | date: "medium" }} - - {{ e.appName }} - - - - - - -
+@if (!loaded()) { + + + {{ "loading" | i18n }} + +} +@if (loaded()) { + + @if (!events() || !events().length) { +

{{ "noEventsInList" | i18n }}

+ } + @if (events() && events().length) { + + + + {{ "timestamp" | i18n }} + {{ "client" | i18n }} + {{ "event" | i18n }} + + + + @for (e of events(); track i; let i = $index) { + + {{ e.date | date: "medium" }} + + {{ e.appName }} + + + + } + + + } + @if (continuationToken) { + + } +
+} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/event-logs/service-accounts-events.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/event-logs/service-accounts-events.component.ts index 5968933064d..525d658f233 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/event-logs/service-accounts-events.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/event-logs/service-accounts-events.component.ts @@ -1,6 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { Component, OnDestroy, OnInit } from "@angular/core"; +import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; import { takeUntil } from "rxjs"; @@ -17,9 +17,8 @@ import { EventExportService } from "@bitwarden/web-vault/app/tools/event-export" import { ServiceAccountEventLogApiService } from "./service-account-event-log-api.service"; -// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush -// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ + changeDetection: ChangeDetectionStrategy.OnPush, selector: "sm-service-accounts-events", templateUrl: "./service-accounts-events.component.html", standalone: false, @@ -69,7 +68,7 @@ export class ServiceAccountEventsComponent async load() { await this.refreshEvents(); - this.loaded = true; + this.loaded.set(true); } protected requestEvents(startDate: string, endDate: string, continuationToken: string) { From a610ce01a23acfffeb087dad9d2d03d8da608a78 Mon Sep 17 00:00:00 2001 From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> Date: Fri, 20 Feb 2026 10:23:59 -0600 Subject: [PATCH 49/89] [PM-31433] Welcome Dialog with Extension Prompt (#18849) * add welcome prompt when extension is not installed * add feature flag * move prompt logic to internal service and add day prompt * rename dialog component * remove feature flag hardcode and add documentation * use i18n for image alt * move state into service * be more explicit when the account or creation date is not available * remove spaces * fix types caused by introducing a numeric feature flag type * add `typeof` for feature flag typing --- .../services/desktop-autofill.service.ts | 2 +- .../navigation-switcher.stories.ts | 2 +- .../product-switcher.stories.ts | 2 +- ...ult-extension-prompt-dialog.component.html | 34 +++ ...-extension-prompt-dialog.component.spec.ts | 86 ++++++ ...vault-extension-prompt-dialog.component.ts | 51 ++++ ...web-vault-extension-prompt.service.spec.ts | 269 ++++++++++++++++++ .../web-vault-extension-prompt.service.ts | 104 +++++++ .../services/web-vault-prompt.service.spec.ts | 21 +- .../services/web-vault-prompt.service.ts | 4 + .../src/images/vault/extension-mock-login.png | Bin 0 -> 170141 bytes apps/web/src/locales/en/messages.json | 15 + libs/common/src/enums/feature-flag.enum.ts | 4 + libs/state/src/core/state-definitions.ts | 7 + 14 files changed, 596 insertions(+), 5 deletions(-) create mode 100644 apps/web/src/app/vault/components/web-vault-extension-prompt/web-vault-extension-prompt-dialog.component.html create mode 100644 apps/web/src/app/vault/components/web-vault-extension-prompt/web-vault-extension-prompt-dialog.component.spec.ts create mode 100644 apps/web/src/app/vault/components/web-vault-extension-prompt/web-vault-extension-prompt-dialog.component.ts create mode 100644 apps/web/src/app/vault/services/web-vault-extension-prompt.service.spec.ts create mode 100644 apps/web/src/app/vault/services/web-vault-extension-prompt.service.ts create mode 100644 apps/web/src/images/vault/extension-mock-login.png diff --git a/apps/desktop/src/autofill/services/desktop-autofill.service.ts b/apps/desktop/src/autofill/services/desktop-autofill.service.ts index cca0097d65e..473ce593cb6 100644 --- a/apps/desktop/src/autofill/services/desktop-autofill.service.ts +++ b/apps/desktop/src/autofill/services/desktop-autofill.service.ts @@ -51,7 +51,7 @@ import type { NativeWindowObject } from "./desktop-fido2-user-interface.service" export class DesktopAutofillService implements OnDestroy { private destroy$ = new Subject(); private registrationRequest: autofill.PasskeyRegistrationRequest; - private featureFlag?: FeatureFlag; + private featureFlag?: typeof FeatureFlag.MacOsNativeCredentialSync; private isEnabled: boolean = false; constructor( diff --git a/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.stories.ts b/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.stories.ts index ba36063fb7b..7af255c6823 100644 --- a/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.stories.ts +++ b/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.stories.ts @@ -105,7 +105,7 @@ class MockBillingAccountProfileStateService implements Partial { getFeatureFlag$(key: Flag): Observable> { - return of(false); + return of(false as FeatureFlagValueType); } } diff --git a/apps/web/src/app/layouts/product-switcher/product-switcher.stories.ts b/apps/web/src/app/layouts/product-switcher/product-switcher.stories.ts index ad18b2b3490..7378e619b1a 100644 --- a/apps/web/src/app/layouts/product-switcher/product-switcher.stories.ts +++ b/apps/web/src/app/layouts/product-switcher/product-switcher.stories.ts @@ -100,7 +100,7 @@ class MockBillingAccountProfileStateService implements Partial { getFeatureFlag$(key: Flag): Observable> { - return of(false); + return of(false as FeatureFlagValueType); } } diff --git a/apps/web/src/app/vault/components/web-vault-extension-prompt/web-vault-extension-prompt-dialog.component.html b/apps/web/src/app/vault/components/web-vault-extension-prompt/web-vault-extension-prompt-dialog.component.html new file mode 100644 index 00000000000..e9932ac9022 --- /dev/null +++ b/apps/web/src/app/vault/components/web-vault-extension-prompt/web-vault-extension-prompt-dialog.component.html @@ -0,0 +1,34 @@ +
+ +
+

+ {{ "extensionPromptHeading" | i18n }} +

+

+ {{ "extensionPromptBody" | i18n }} +

+
+ + + + {{ "downloadExtension" | i18n }} + + +
+
+
diff --git a/apps/web/src/app/vault/components/web-vault-extension-prompt/web-vault-extension-prompt-dialog.component.spec.ts b/apps/web/src/app/vault/components/web-vault-extension-prompt/web-vault-extension-prompt-dialog.component.spec.ts new file mode 100644 index 00000000000..fdf218d8c35 --- /dev/null +++ b/apps/web/src/app/vault/components/web-vault-extension-prompt/web-vault-extension-prompt-dialog.component.spec.ts @@ -0,0 +1,86 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { provideNoopAnimations } from "@angular/platform-browser/animations"; +import { mock, MockProxy } from "jest-mock-extended"; + +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { DeviceType } from "@bitwarden/common/enums"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { mockAccountServiceWith } from "@bitwarden/common/spec"; +import { UserId } from "@bitwarden/common/types/guid"; +import { DialogRef, DialogService } from "@bitwarden/components"; + +import { WebVaultExtensionPromptService } from "../../services/web-vault-extension-prompt.service"; + +import { WebVaultExtensionPromptDialogComponent } from "./web-vault-extension-prompt-dialog.component"; + +describe("WebVaultExtensionPromptDialogComponent", () => { + let component: WebVaultExtensionPromptDialogComponent; + let fixture: ComponentFixture; + let mockDialogRef: MockProxy>; + + const mockUserId = "test-user-id" as UserId; + + const getDevice = jest.fn(() => DeviceType.ChromeBrowser); + const mockUpdate = jest.fn().mockResolvedValue(undefined); + + const getDialogDismissedState = jest.fn().mockReturnValue({ + update: mockUpdate, + }); + + beforeEach(async () => { + const mockAccountService = mockAccountServiceWith(mockUserId); + mockDialogRef = mock>(); + + await TestBed.configureTestingModule({ + imports: [WebVaultExtensionPromptDialogComponent], + providers: [ + provideNoopAnimations(), + { + provide: PlatformUtilsService, + useValue: { getDevice }, + }, + { provide: I18nService, useValue: { t: (key: string) => key } }, + { provide: AccountService, useValue: mockAccountService }, + { provide: DialogRef, useValue: mockDialogRef }, + { provide: DialogService, useValue: mock() }, + { + provide: WebVaultExtensionPromptService, + useValue: { getDialogDismissedState }, + }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(WebVaultExtensionPromptDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + describe("ngOnInit", () => { + it("sets webStoreUrl", () => { + expect(getDevice).toHaveBeenCalled(); + + expect(component["webStoreUrl"]).toBe( + "https://chromewebstore.google.com/detail/bitwarden-password-manage/nngceckbapebfimnlniiiahkandclblb", + ); + }); + }); + + describe("dismissPrompt", () => { + it("calls webVaultExtensionPromptService.getDialogDismissedState and updates to true", async () => { + await component.dismissPrompt(); + + expect(getDialogDismissedState).toHaveBeenCalledWith(mockUserId); + expect(mockUpdate).toHaveBeenCalledWith(expect.any(Function)); + + const updateFn = mockUpdate.mock.calls[0][0]; + expect(updateFn()).toBe(true); + }); + + it("closes the dialog", async () => { + await component.dismissPrompt(); + + expect(mockDialogRef.close).toHaveBeenCalled(); + }); + }); +}); diff --git a/apps/web/src/app/vault/components/web-vault-extension-prompt/web-vault-extension-prompt-dialog.component.ts b/apps/web/src/app/vault/components/web-vault-extension-prompt/web-vault-extension-prompt-dialog.component.ts new file mode 100644 index 00000000000..e5dcf5e3cf2 --- /dev/null +++ b/apps/web/src/app/vault/components/web-vault-extension-prompt/web-vault-extension-prompt-dialog.component.ts @@ -0,0 +1,51 @@ +import { CommonModule } from "@angular/common"; +import { Component, ChangeDetectionStrategy, OnInit } from "@angular/core"; +import { firstValueFrom } from "rxjs"; + +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { getWebStoreUrl } from "@bitwarden/common/vault/utils/get-web-store-url"; +import { + ButtonModule, + DialogModule, + DialogRef, + DialogService, + IconComponent, +} from "@bitwarden/components"; +import { I18nPipe } from "@bitwarden/ui-common"; + +import { WebVaultExtensionPromptService } from "../../services/web-vault-extension-prompt.service"; + +@Component({ + selector: "web-vault-extension-prompt-dialog", + templateUrl: "./web-vault-extension-prompt-dialog.component.html", + imports: [CommonModule, ButtonModule, DialogModule, I18nPipe, IconComponent], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class WebVaultExtensionPromptDialogComponent implements OnInit { + constructor( + private platformUtilsService: PlatformUtilsService, + private accountService: AccountService, + private dialogRef: DialogRef, + private webVaultExtensionPromptService: WebVaultExtensionPromptService, + ) {} + + /** Download Url for the extension based on the browser */ + protected webStoreUrl: string = ""; + + ngOnInit(): void { + this.webStoreUrl = getWebStoreUrl(this.platformUtilsService.getDevice()); + } + + async dismissPrompt() { + const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + await this.webVaultExtensionPromptService.getDialogDismissedState(userId).update(() => true); + this.dialogRef.close(); + } + + /** Opens the web extension prompt generator dialog. */ + static open(dialogService: DialogService) { + return dialogService.open(WebVaultExtensionPromptDialogComponent); + } +} diff --git a/apps/web/src/app/vault/services/web-vault-extension-prompt.service.spec.ts b/apps/web/src/app/vault/services/web-vault-extension-prompt.service.spec.ts new file mode 100644 index 00000000000..4a8865990df --- /dev/null +++ b/apps/web/src/app/vault/services/web-vault-extension-prompt.service.spec.ts @@ -0,0 +1,269 @@ +import { TestBed } from "@angular/core/testing"; +import { BehaviorSubject } from "rxjs"; + +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { UserId } from "@bitwarden/common/types/guid"; +import { DialogService } from "@bitwarden/components"; +import { StateProvider } from "@bitwarden/state"; + +import { WebVaultExtensionPromptDialogComponent } from "../components/web-vault-extension-prompt/web-vault-extension-prompt-dialog.component"; + +import { WebBrowserInteractionService } from "./web-browser-interaction.service"; +import { WebVaultExtensionPromptService } from "./web-vault-extension-prompt.service"; + +describe("WebVaultExtensionPromptService", () => { + let service: WebVaultExtensionPromptService; + + const mockUserId = "user-123" as UserId; + const mockAccountCreationDate = new Date("2026-01-15"); + + const getFeatureFlag = jest.fn(); + const extensionInstalled$ = new BehaviorSubject(false); + const mockStateSubject = new BehaviorSubject(false); + const activeAccountSubject = new BehaviorSubject<{ id: UserId; creationDate: Date | null }>({ + id: mockUserId, + creationDate: mockAccountCreationDate, + }); + const getUser = jest.fn().mockReturnValue({ state$: mockStateSubject.asObservable() }); + + beforeEach(() => { + jest.clearAllMocks(); + getFeatureFlag.mockResolvedValue(false); + extensionInstalled$.next(false); + mockStateSubject.next(false); + activeAccountSubject.next({ id: mockUserId, creationDate: mockAccountCreationDate }); + + TestBed.configureTestingModule({ + providers: [ + WebVaultExtensionPromptService, + { + provide: StateProvider, + useValue: { + getUser, + }, + }, + { + provide: WebBrowserInteractionService, + useValue: { + extensionInstalled$: extensionInstalled$.asObservable(), + }, + }, + { + provide: AccountService, + useValue: { + activeAccount$: activeAccountSubject.asObservable(), + }, + }, + { + provide: ConfigService, + useValue: { + getFeatureFlag, + }, + }, + { + provide: DialogService, + useValue: { + open: jest.fn(), + }, + }, + ], + }); + + service = TestBed.inject(WebVaultExtensionPromptService); + }); + + describe("conditionallyPromptUserForExtension", () => { + it("returns false when feature flag is disabled", async () => { + getFeatureFlag.mockResolvedValueOnce(false); + + const result = await service.conditionallyPromptUserForExtension(mockUserId); + + expect(result).toBe(false); + expect(getFeatureFlag).toHaveBeenCalledWith( + FeatureFlag.PM29438_WelcomeDialogWithExtensionPrompt, + ); + }); + + it("returns false when dialog has been dismissed", async () => { + getFeatureFlag.mockResolvedValueOnce(true); + mockStateSubject.next(true); + extensionInstalled$.next(false); + + const result = await service.conditionallyPromptUserForExtension(mockUserId); + + expect(result).toBe(false); + }); + + it("returns false when profile is not within thresholds (too old)", async () => { + getFeatureFlag + .mockResolvedValueOnce(true) // Main feature flag + .mockResolvedValueOnce(0); // Min age days + mockStateSubject.next(false); + extensionInstalled$.next(false); + const oldAccountDate = new Date("2025-12-01"); // More than 30 days old + activeAccountSubject.next({ id: mockUserId, creationDate: oldAccountDate }); + + const result = await service.conditionallyPromptUserForExtension(mockUserId); + + expect(result).toBe(false); + }); + + it("returns false when profile is not within thresholds (too young)", async () => { + getFeatureFlag + .mockResolvedValueOnce(true) // Main feature flag + .mockResolvedValueOnce(10); // Min age days = 10 + mockStateSubject.next(false); + extensionInstalled$.next(false); + const youngAccountDate = new Date(); // Today + youngAccountDate.setDate(youngAccountDate.getDate() - 5); // 5 days old + activeAccountSubject.next({ id: mockUserId, creationDate: youngAccountDate }); + + const result = await service.conditionallyPromptUserForExtension(mockUserId); + + expect(result).toBe(false); + }); + + it("returns false when extension is installed", async () => { + getFeatureFlag + .mockResolvedValueOnce(true) // Main feature flag + .mockResolvedValueOnce(0); // Min age days + mockStateSubject.next(false); + extensionInstalled$.next(true); + + const result = await service.conditionallyPromptUserForExtension(mockUserId); + + expect(result).toBe(false); + }); + + it("returns true and opens dialog when all conditions are met", async () => { + getFeatureFlag + .mockResolvedValueOnce(true) // Main feature flag + .mockResolvedValueOnce(0); // Min age days + mockStateSubject.next(false); + extensionInstalled$.next(false); + + // Set account creation date to be within threshold (15 days old) + const validCreationDate = new Date(); + validCreationDate.setDate(validCreationDate.getDate() - 15); + activeAccountSubject.next({ id: mockUserId, creationDate: validCreationDate }); + + const dialogClosedSubject = new BehaviorSubject(undefined); + const openSpy = jest + .spyOn(WebVaultExtensionPromptDialogComponent, "open") + .mockReturnValue({ closed: dialogClosedSubject.asObservable() } as any); + + const resultPromise = service.conditionallyPromptUserForExtension(mockUserId); + + // Close the dialog + dialogClosedSubject.next(undefined); + + const result = await resultPromise; + + expect(openSpy).toHaveBeenCalledWith(expect.anything()); + expect(result).toBe(true); + }); + }); + + describe("profileIsWithinThresholds", () => { + it("returns false when account is younger than min threshold", async () => { + const minAgeDays = 7; + getFeatureFlag.mockResolvedValueOnce(minAgeDays); + + const recentDate = new Date(); + recentDate.setDate(recentDate.getDate() - 5); // 5 days old + activeAccountSubject.next({ id: mockUserId, creationDate: recentDate }); + + const result = await service["profileIsWithinThresholds"](); + + expect(result).toBe(false); + }); + + it("returns true when account is exactly at min threshold", async () => { + const minAgeDays = 7; + getFeatureFlag.mockResolvedValueOnce(minAgeDays); + + const exactDate = new Date(); + exactDate.setDate(exactDate.getDate() - 7); // Exactly 7 days old + activeAccountSubject.next({ id: mockUserId, creationDate: exactDate }); + + const result = await service["profileIsWithinThresholds"](); + + expect(result).toBe(true); + }); + + it("returns true when account is within the thresholds", async () => { + const minAgeDays = 0; + getFeatureFlag.mockResolvedValueOnce(minAgeDays); + + const validDate = new Date(); + validDate.setDate(validDate.getDate() - 15); // 15 days old (between 0 and 30) + activeAccountSubject.next({ id: mockUserId, creationDate: validDate }); + + const result = await service["profileIsWithinThresholds"](); + + expect(result).toBe(true); + }); + + it("returns false when account is older than max threshold (30 days)", async () => { + const minAgeDays = 0; + getFeatureFlag.mockResolvedValueOnce(minAgeDays); + + const oldDate = new Date(); + oldDate.setDate(oldDate.getDate() - 31); // 31 days old + activeAccountSubject.next({ id: mockUserId, creationDate: oldDate }); + + const result = await service["profileIsWithinThresholds"](); + + expect(result).toBe(false); + }); + + it("returns false when account is exactly 30 days old", async () => { + const minAgeDays = 0; + getFeatureFlag.mockResolvedValueOnce(minAgeDays); + + const exactDate = new Date(); + exactDate.setDate(exactDate.getDate() - 30); // Exactly 30 days old + activeAccountSubject.next({ id: mockUserId, creationDate: exactDate }); + + const result = await service["profileIsWithinThresholds"](); + + expect(result).toBe(false); + }); + + it("uses default min age of 0 when feature flag is null", async () => { + getFeatureFlag.mockResolvedValueOnce(null); + + const recentDate = new Date(); + recentDate.setDate(recentDate.getDate() - 5); // 5 days old + activeAccountSubject.next({ id: mockUserId, creationDate: recentDate }); + + const result = await service["profileIsWithinThresholds"](); + + expect(result).toBe(true); + }); + + it("defaults to false", async () => { + getFeatureFlag.mockResolvedValueOnce(0); + activeAccountSubject.next({ id: mockUserId, creationDate: null }); + + const result = await service["profileIsWithinThresholds"](); + + expect(result).toBe(false); + }); + }); + + describe("getDialogDismissedState", () => { + it("returns the SingleUserState for the dialog dismissed state", () => { + service.getDialogDismissedState(mockUserId); + + expect(getUser).toHaveBeenCalledWith( + mockUserId, + expect.objectContaining({ + key: "vaultWelcomeExtensionDialogDismissed", + }), + ); + }); + }); +}); diff --git a/apps/web/src/app/vault/services/web-vault-extension-prompt.service.ts b/apps/web/src/app/vault/services/web-vault-extension-prompt.service.ts new file mode 100644 index 00000000000..3e13935f94c --- /dev/null +++ b/apps/web/src/app/vault/services/web-vault-extension-prompt.service.ts @@ -0,0 +1,104 @@ +import { inject, Injectable } from "@angular/core"; +import { firstValueFrom, map } from "rxjs"; + +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { UserId } from "@bitwarden/common/types/guid"; +import { DialogService } from "@bitwarden/components"; +import { StateProvider, UserKeyDefinition, WELCOME_EXTENSION_DIALOG_DISK } from "@bitwarden/state"; + +import { WebVaultExtensionPromptDialogComponent } from "../components/web-vault-extension-prompt/web-vault-extension-prompt-dialog.component"; + +import { WebBrowserInteractionService } from "./web-browser-interaction.service"; + +export const WELCOME_EXTENSION_DIALOG_DISMISSED = new UserKeyDefinition( + WELCOME_EXTENSION_DIALOG_DISK, + "vaultWelcomeExtensionDialogDismissed", + { + deserializer: (dismissed) => dismissed, + clearOn: [], + }, +); + +@Injectable({ providedIn: "root" }) +export class WebVaultExtensionPromptService { + private stateProvider = inject(StateProvider); + private webBrowserInteractionService = inject(WebBrowserInteractionService); + private accountService = inject(AccountService); + private configService = inject(ConfigService); + private dialogService = inject(DialogService); + + /** + * Conditionally prompts the user to install the web extension + */ + async conditionallyPromptUserForExtension(userId: UserId) { + const featureFlagEnabled = await this.configService.getFeatureFlag( + FeatureFlag.PM29438_WelcomeDialogWithExtensionPrompt, + ); + + if (!featureFlagEnabled) { + return false; + } + + // Extension check takes time, trigger it early + const hasExtensionInstalled = firstValueFrom( + this.webBrowserInteractionService.extensionInstalled$, + ); + + const hasDismissedExtensionPrompt = await firstValueFrom( + this.getDialogDismissedState(userId).state$.pipe(map((dismissed) => dismissed ?? false)), + ); + if (hasDismissedExtensionPrompt) { + return false; + } + + const profileIsWithinThresholds = await this.profileIsWithinThresholds(); + if (!profileIsWithinThresholds) { + return false; + } + + if (await hasExtensionInstalled) { + return false; + } + + const dialogRef = WebVaultExtensionPromptDialogComponent.open(this.dialogService); + await firstValueFrom(dialogRef.closed); + + return true; + } + + /** Returns the SingleUserState for the dialog dismissed state */ + getDialogDismissedState(userId: UserId) { + return this.stateProvider.getUser(userId, WELCOME_EXTENSION_DIALOG_DISMISSED); + } + + /** + * Returns true if the user's profile is within the defined thresholds for showing the extension prompt, false otherwise. + * Thresholds are defined as account age between a configurable number of days and 30 days. + */ + private async profileIsWithinThresholds() { + const creationDate = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((account) => account?.creationDate)), + ); + + // When account or creationDate is not available for some reason, + // default to not showing the prompt to avoid disrupting the user. + if (!creationDate) { + return false; + } + + const minAccountAgeDays = await this.configService.getFeatureFlag( + FeatureFlag.PM29438_DialogWithExtensionPromptAccountAge, + ); + + const now = new Date(); + const accountAgeMs = now.getTime() - creationDate.getTime(); + const accountAgeDays = accountAgeMs / (1000 * 60 * 60 * 24); + + const minAgeDays = minAccountAgeDays ?? 0; + const maxAgeDays = 30; + + return accountAgeDays >= minAgeDays && accountAgeDays < maxAgeDays; + } +} diff --git a/apps/web/src/app/vault/services/web-vault-prompt.service.spec.ts b/apps/web/src/app/vault/services/web-vault-prompt.service.spec.ts index eb72c80fe04..14bbc1a86d5 100644 --- a/apps/web/src/app/vault/services/web-vault-prompt.service.spec.ts +++ b/apps/web/src/app/vault/services/web-vault-prompt.service.spec.ts @@ -20,6 +20,7 @@ import { } from "../../admin-console/organizations/policies"; import { UnifiedUpgradePromptService } from "../../billing/individual/upgrade/services"; +import { WebVaultExtensionPromptService } from "./web-vault-extension-prompt.service"; import { WebVaultPromptService } from "./web-vault-prompt.service"; import { WelcomeDialogService } from "./welcome-dialog.service"; @@ -43,6 +44,7 @@ describe("WebVaultPromptService", () => { const enforceOrganizationDataOwnership = jest.fn().mockResolvedValue(undefined); const conditionallyShowWelcomeDialog = jest.fn().mockResolvedValue(false); const logError = jest.fn(); + const conditionallyPromptUserForExtension = jest.fn().mockResolvedValue(false); let activeAccount$: BehaviorSubject; @@ -74,7 +76,14 @@ describe("WebVaultPromptService", () => { { provide: ConfigService, useValue: { getFeatureFlag$ } }, { provide: DialogService, useValue: { open } }, { provide: LogService, useValue: { error: logError } }, - { provide: WelcomeDialogService, useValue: { conditionallyShowWelcomeDialog } }, + { + provide: WebVaultExtensionPromptService, + useValue: { conditionallyPromptUserForExtension }, + }, + { + provide: WelcomeDialogService, + useValue: { conditionallyShowWelcomeDialog, conditionallyPromptUserForExtension }, + }, ], }); @@ -97,11 +106,19 @@ describe("WebVaultPromptService", () => { service["vaultItemTransferService"].enforceOrganizationDataOwnership, ).toHaveBeenCalledWith(mockUserId); }); + + it("calls conditionallyPromptUserForExtension with the userId", async () => { + await service.conditionallyPromptUser(); + + expect( + service["webVaultExtensionPromptService"].conditionallyPromptUserForExtension, + ).toHaveBeenCalledWith(mockUserId); + }); }); describe("setupAutoConfirm", () => { it("shows dialog when all conditions are met", fakeAsync(() => { - getFeatureFlag$.mockReturnValueOnce(of(true)); + getFeatureFlag$.mockReturnValue(of(true)); configurationAutoConfirm$.mockReturnValueOnce( of({ showSetupDialog: true, enabled: false, showBrowserNotification: false }), ); diff --git a/apps/web/src/app/vault/services/web-vault-prompt.service.ts b/apps/web/src/app/vault/services/web-vault-prompt.service.ts index 4c4e7a3fe78..aac30e7d0f4 100644 --- a/apps/web/src/app/vault/services/web-vault-prompt.service.ts +++ b/apps/web/src/app/vault/services/web-vault-prompt.service.ts @@ -20,6 +20,7 @@ import { } from "../../admin-console/organizations/policies"; import { UnifiedUpgradePromptService } from "../../billing/individual/upgrade/services"; +import { WebVaultExtensionPromptService } from "./web-vault-extension-prompt.service"; import { WelcomeDialogService } from "./welcome-dialog.service"; @Injectable() @@ -33,6 +34,7 @@ export class WebVaultPromptService { private configService = inject(ConfigService); private dialogService = inject(DialogService); private logService = inject(LogService); + private webVaultExtensionPromptService = inject(WebVaultExtensionPromptService); private welcomeDialogService = inject(WelcomeDialogService); private userId$ = this.accountService.activeAccount$.pipe(getUserId); @@ -57,6 +59,8 @@ export class WebVaultPromptService { await this.welcomeDialogService.conditionallyShowWelcomeDialog(); + await this.webVaultExtensionPromptService.conditionallyPromptUserForExtension(userId); + this.checkForAutoConfirm(); } diff --git a/apps/web/src/images/vault/extension-mock-login.png b/apps/web/src/images/vault/extension-mock-login.png new file mode 100644 index 0000000000000000000000000000000000000000..e002da6db2d494b90485d8008c59ffb241cbb576 GIT binary patch literal 170141 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�mG9faX5r{58M!1e0!_Ni%@Sp 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 literal 0 HcmV?d00001 diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 2d9cba6d409..ef8c109bc4b 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index 6fdb146beb8..0cd97eb7f2e 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -70,6 +70,8 @@ export enum FeatureFlag { BrowserPremiumSpotlight = "pm-23384-browser-premium-spotlight", MigrateMyVaultToMyItems = "pm-20558-migrate-myvault-to-myitems", PM27632_SdkCipherCrudOperations = "pm-27632-cipher-crud-operations-to-sdk", + PM29438_WelcomeDialogWithExtensionPrompt = "pm-29438-welcome-dialog-with-extension-prompt", + PM29438_DialogWithExtensionPromptAccountAge = "pm-29438-dialog-with-extension-prompt-account-age", PM29437_WelcomeDialog = "pm-29437-welcome-dialog-no-ext-prompt", PM31039ItemActionInExtension = "pm-31039-item-action-in-extension", @@ -137,6 +139,8 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.BrowserPremiumSpotlight]: FALSE, [FeatureFlag.PM27632_SdkCipherCrudOperations]: FALSE, [FeatureFlag.MigrateMyVaultToMyItems]: FALSE, + [FeatureFlag.PM29438_WelcomeDialogWithExtensionPrompt]: FALSE, + [FeatureFlag.PM29438_DialogWithExtensionPromptAccountAge]: 5, [FeatureFlag.PM29437_WelcomeDialog]: FALSE, /* Auth */ diff --git a/libs/state/src/core/state-definitions.ts b/libs/state/src/core/state-definitions.ts index 30ee2be0592..33c9c780dec 100644 --- a/libs/state/src/core/state-definitions.ts +++ b/libs/state/src/core/state-definitions.ts @@ -220,6 +220,13 @@ export const VAULT_BROWSER_INTRO_CAROUSEL = new StateDefinition( "disk", ); export const VAULT_AT_RISK_PASSWORDS_MEMORY = new StateDefinition("vaultAtRiskPasswords", "memory"); +export const WELCOME_EXTENSION_DIALOG_DISK = new StateDefinition( + "vaultWelcomeExtensionDialogDismissed", + "disk", + { + web: "disk-local", + }, +); // KM From c6234076218944c120a4fe3f9e3d0707250d6d4d Mon Sep 17 00:00:00 2001 From: John Harrington <84741727+harr1424@users.noreply.github.com> Date: Fri, 20 Feb 2026 09:31:49 -0700 Subject: [PATCH 50/89] [PM-32471] [Defect] Importers have regressed during folder migration (#19079) * relax type-checking and add importer test coverage * satisfy lint --- .../models/request/folder-with-id.request.ts | 15 ++- .../bitwarden/bitwarden-csv-importer.spec.ts | 102 ++++++++++++++++++ .../src/services/import.service.spec.ts | 21 ++++ 3 files changed, 137 insertions(+), 1 deletion(-) diff --git a/libs/common/src/vault/models/request/folder-with-id.request.ts b/libs/common/src/vault/models/request/folder-with-id.request.ts index 8af890048ba..aecb12a05fc 100644 --- a/libs/common/src/vault/models/request/folder-with-id.request.ts +++ b/libs/common/src/vault/models/request/folder-with-id.request.ts @@ -1,12 +1,25 @@ +// FIXME: Update this file to be type safe and remove this and next line +// @ts-strict-ignore import { Folder } from "../domain/folder"; import { FolderRequest } from "./folder.request"; export class FolderWithIdRequest extends FolderRequest { + /** + * Declared as `string` (not `string | null`) to satisfy the + * {@link UserKeyRotationDataProvider}`` + * constraint on `FolderService`. + * + * At runtime this is `null` for new import folders. PR #17077 enforced strict type-checking on + * folder models, changing this assignment to `folder.id ?? ""` — causing the importer to send + * `{"id":""}` instead of `{"id":null}`, which the server rejected. + * The `|| null` below restores the pre-migration behavior while `@ts-strict-ignore` above + * allows the `null` assignment against the `string` declaration. + */ id: string; constructor(folder: Folder) { super(folder); - this.id = folder.id ?? ""; + this.id = folder.id || null; } } diff --git a/libs/importer/src/importers/bitwarden/bitwarden-csv-importer.spec.ts b/libs/importer/src/importers/bitwarden/bitwarden-csv-importer.spec.ts index 8f1a281050f..44ee35568d0 100644 --- a/libs/importer/src/importers/bitwarden/bitwarden-csv-importer.spec.ts +++ b/libs/importer/src/importers/bitwarden/bitwarden-csv-importer.spec.ts @@ -120,4 +120,106 @@ describe("BitwardenCsvImporter", () => { expect(result.ciphers.length).toBe(1); expect(result.ciphers[0].archivedDate).toBeUndefined(); }); + + describe("Individual vault imports with folders", () => { + beforeEach(() => { + importer.organizationId = null; + }); + + it("should parse folder and create a folder relationship", async () => { + const data = + `folder,favorite,type,name,login_uri,login_username,login_password` + + `\nSocial,0,login,Facebook,https://facebook.com,user@example.com,password`; + + const result = await importer.parse(data); + + expect(result.success).toBe(true); + expect(result.ciphers.length).toBe(1); + expect(result.folders.length).toBe(1); + expect(result.folders[0].name).toBe("Social"); + expect(result.folderRelationships).toHaveLength(1); + expect(result.folderRelationships[0]).toEqual([0, 0]); + }); + + it("should deduplicate folders when multiple items share the same folder", async () => { + const data = + `folder,favorite,type,name,login_uri,login_username,login_password` + + `\nSocial,0,login,Facebook,https://facebook.com,user1,pass1` + + `\nSocial,0,login,Twitter,https://twitter.com,user2,pass2`; + + const result = await importer.parse(data); + + expect(result.success).toBe(true); + expect(result.ciphers.length).toBe(2); + expect(result.folders.length).toBe(1); + expect(result.folders[0].name).toBe("Social"); + expect(result.folderRelationships).toHaveLength(2); + expect(result.folderRelationships[0]).toEqual([0, 0]); + expect(result.folderRelationships[1]).toEqual([1, 0]); + }); + + it("should create parent folders for nested folder paths", async () => { + const data = + `folder,favorite,type,name,login_uri,login_username,login_password` + + `\nWork/Email,0,login,Gmail,https://gmail.com,user@work.com,pass`; + + const result = await importer.parse(data); + + expect(result.success).toBe(true); + expect(result.folders.length).toBe(2); + expect(result.folders.map((f) => f.name)).toContain("Work/Email"); + expect(result.folders.map((f) => f.name)).toContain("Work"); + expect(result.folderRelationships).toHaveLength(1); + expect(result.folderRelationships[0]).toEqual([0, 0]); + }); + + it("should create no folder or relationship when folder column is empty", async () => { + const data = + `folder,favorite,type,name,login_uri,login_username,login_password` + + `\n,0,login,No Folder Item,https://example.com,user,pass`; + + const result = await importer.parse(data); + + expect(result.success).toBe(true); + expect(result.ciphers.length).toBe(1); + expect(result.folders.length).toBe(0); + expect(result.folderRelationships).toHaveLength(0); + }); + }); + + describe("organization collection import", () => { + it("should set collectionRelationships mapping ciphers to collections", async () => { + const data = + `collections,type,name,login_uri,login_username,login_password` + + `\ncol1,login,Item1,https://example.com,user1,pass1` + + `\ncol2,login,Item2,https://example.com,user2,pass2`; + + const result = await importer.parse(data); + + expect(result.success).toBe(true); + expect(result.ciphers.length).toBe(2); + expect(result.collections.length).toBe(2); + // Each cipher maps to its own collection + expect(result.collectionRelationships).toHaveLength(2); + expect(result.collectionRelationships[0]).toEqual([0, 0]); + expect(result.collectionRelationships[1]).toEqual([1, 1]); + }); + + it("should deduplicate collections and map both ciphers to the shared collection", async () => { + const data = + `collections,type,name,login_uri,login_username,login_password` + + `\nShared,login,Item1,https://example.com,user1,pass1` + + `\nShared,login,Item2,https://example.com,user2,pass2`; + + const result = await importer.parse(data); + + expect(result.success).toBe(true); + expect(result.ciphers.length).toBe(2); + expect(result.collections.length).toBe(1); + expect(result.collections[0].name).toBe("Shared"); + expect(result.collectionRelationships).toHaveLength(2); + expect(result.collectionRelationships[0]).toEqual([0, 0]); + expect(result.collectionRelationships[1]).toEqual([1, 0]); + }); + }); }); diff --git a/libs/importer/src/services/import.service.spec.ts b/libs/importer/src/services/import.service.spec.ts index 33a1e47a4ce..5f9b3c4b085 100644 --- a/libs/importer/src/services/import.service.spec.ts +++ b/libs/importer/src/services/import.service.spec.ts @@ -15,6 +15,8 @@ import { Utils } from "@bitwarden/common/platform/misc/utils"; import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; +import { Folder } from "@bitwarden/common/vault/models/domain/folder"; +import { FolderWithIdRequest } from "@bitwarden/common/vault/models/request/folder-with-id.request"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service"; @@ -280,6 +282,25 @@ describe("ImportService", () => { }); }); +describe("FolderWithIdRequest", () => { + function makeFolder(id: string): Folder { + const folder = new Folder(); + folder.id = id; + return folder; + } + + it("preserves a real folder id", () => { + const guid = "f1a2b3c4-d5e6-7890-abcd-ef1234567890"; + const request = new FolderWithIdRequest(makeFolder(guid)); + expect(request.id).toBe(guid); + }); + + it("sends null when folder id is empty string (new import folder)", () => { + const request = new FolderWithIdRequest(makeFolder("")); + expect(request.id).toBeNull(); + }); +}); + function createCipher(options: Partial = {}) { const cipher = new CipherView(); From 99fdaaec91d57c13ce541e86d4754bcf7a57ad07 Mon Sep 17 00:00:00 2001 From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> Date: Fri, 20 Feb 2026 11:03:46 -0600 Subject: [PATCH 51/89] cast feature flag mock (#19106) --- libs/components/src/a11y/router-focus-manager.service.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/components/src/a11y/router-focus-manager.service.spec.ts b/libs/components/src/a11y/router-focus-manager.service.spec.ts index 236a05ac038..f7b78296ccb 100644 --- a/libs/components/src/a11y/router-focus-manager.service.spec.ts +++ b/libs/components/src/a11y/router-focus-manager.service.spec.ts @@ -72,7 +72,7 @@ describe("RouterFocusManagerService", () => { return featureFlagSubject.asObservable(); } return new BehaviorSubject(false).asObservable(); - }), + }) as ConfigService["getFeatureFlag$"], }; // Spy on document.querySelector and console.warn From 38bcc9239858ad236f8fa33ecbb0a4cc293b6e76 Mon Sep 17 00:00:00 2001 From: John Harrington <84741727+harr1424@users.noreply.github.com> Date: Fri, 20 Feb 2026 11:03:13 -0700 Subject: [PATCH 52/89] reset otp state on back nav to email input (#19105) --- apps/web/src/app/tools/send/send-access/send-auth.component.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/web/src/app/tools/send/send-access/send-auth.component.ts b/apps/web/src/app/tools/send/send-access/send-auth.component.ts index 97b71778539..7617b0a502e 100644 --- a/apps/web/src/app/tools/send/send-access/send-auth.component.ts +++ b/apps/web/src/app/tools/send/send-access/send-auth.component.ts @@ -92,6 +92,7 @@ export class SendAuthComponent implements OnInit { onBackToEmail() { this.enterOtp.set(false); + this.otpSubmitted = false; this.updatePageTitle(); } From 0569ec95172a13de9b8340be37229119d49dd6f5 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Fri, 20 Feb 2026 19:09:52 +0100 Subject: [PATCH 53/89] [PM-31880 | BEEEP] Move random out of keyservice to PureCrypto (#18838) * Move random to PureCrypto * Rename and fix builds * Fix tests * Fix build * Prettier * Fix tests --- .../src/abstractions/key.service.ts | 1 - libs/key-management/src/key.service.ts | 35 -------- .../src/generator-services.module.ts | 2 +- .../core/src/engine/email-calculator.spec.ts | 4 + .../core/src/engine/email-randomizer.spec.ts | 4 + .../core/src/engine/forwarder-context.spec.ts | 4 + libs/tools/generator/core/src/engine/index.ts | 2 +- .../src/engine/password-randomizer.spec.ts | 4 + ....spec.ts => purecrypto-randomizer.spec.ts} | 87 ++++++++++--------- ...randomizer.ts => purecrypto-randomizer.ts} | 22 +++-- .../src/engine/username-randomizer.spec.ts | 4 + libs/tools/generator/core/src/factories.ts | 8 +- .../core/src/metadata/email/catchall.spec.ts | 4 + .../src/metadata/email/plus-address.spec.ts | 4 + .../metadata/password/eff-word-list.spec.ts | 4 + .../metadata/password/random-password.spec.ts | 4 + .../metadata/username/eff-word-list.spec.ts | 4 + .../available-algorithms-policy.spec.ts | 4 + ...ynamic-password-policy-constraints.spec.ts | 4 + .../passphrase-policy-constraints.spec.ts | 4 + .../providers/credential-preferences.spec.ts | 4 + .../generator-metadata-provider.spec.ts | 4 + ...fault-credential-generator.service.spec.ts | 4 + .../catchall-generator-strategy.spec.ts | 4 + .../eff-username-generator-strategy.spec.ts | 4 + .../forwarder-generator-strategy.spec.ts | 4 + .../passphrase-generator-strategy.spec.ts | 4 + .../password-generator-strategy.spec.ts | 4 + .../subaddress-generator-strategy.spec.ts | 4 + .../src/types/generated-credential.spec.ts | 4 + .../history/src/generated-credential.spec.ts | 4 + .../local-generator-history.service.spec.ts | 4 + ...eate-legacy-password-generation-service.ts | 4 +- ...eate-legacy-username-generation-service.ts | 4 +- ...legacy-password-generation.service.spec.ts | 4 + ...legacy-username-generation.service.spec.ts | 4 + ...fault-generator-navigation.service.spec.ts | 4 + .../generator-navigation-evaluator.spec.ts | 4 + 38 files changed, 187 insertions(+), 94 deletions(-) rename libs/tools/generator/core/src/engine/{key-service-randomizer.spec.ts => purecrypto-randomizer.spec.ts} (59%) rename libs/tools/generator/core/src/engine/{key-service-randomizer.ts => purecrypto-randomizer.ts} (73%) diff --git a/libs/key-management/src/abstractions/key.service.ts b/libs/key-management/src/abstractions/key.service.ts index 2dedc78a027..e9844ede4bb 100644 --- a/libs/key-management/src/abstractions/key.service.ts +++ b/libs/key-management/src/abstractions/key.service.ts @@ -316,7 +316,6 @@ export abstract class KeyService { * @throws Error when provided userId is null or undefined */ abstract clearKeys(userId: UserId): Promise; - abstract randomNumber(min: number, max: number): Promise; /** * Generates a new cipher key * @returns A new cipher key diff --git a/libs/key-management/src/key.service.ts b/libs/key-management/src/key.service.ts index 7b4e8d83127..7258857d889 100644 --- a/libs/key-management/src/key.service.ts +++ b/libs/key-management/src/key.service.ts @@ -493,41 +493,6 @@ export class DefaultKeyService implements KeyServiceAbstraction { await this.accountCryptographyStateService.clearAccountCryptographicState(userId); } - // EFForg/OpenWireless - // ref https://github.com/EFForg/OpenWireless/blob/master/app/js/diceware.js - async randomNumber(min: number, max: number): Promise { - let rval = 0; - const range = max - min + 1; - const bitsNeeded = Math.ceil(Math.log2(range)); - if (bitsNeeded > 53) { - throw new Error("We cannot generate numbers larger than 53 bits."); - } - - const bytesNeeded = Math.ceil(bitsNeeded / 8); - const mask = Math.pow(2, bitsNeeded) - 1; - // 7776 -> (2^13 = 8192) -1 == 8191 or 0x00001111 11111111 - - // Fill a byte array with N random numbers - const byteArray = new Uint8Array(await this.cryptoFunctionService.randomBytes(bytesNeeded)); - - let p = (bytesNeeded - 1) * 8; - for (let i = 0; i < bytesNeeded; i++) { - rval += byteArray[i] * Math.pow(2, p); - p -= 8; - } - - // Use & to apply the mask and reduce the number of recursive lookups - rval = rval & mask; - - if (rval >= range) { - // Integer out of acceptable range - return this.randomNumber(min, max); - } - - // Return an integer that falls within the range - return min + rval; - } - // ---HELPERS--- async validateUserKey(key: UserKey | MasterKey | null, userId: UserId): Promise { if (key == null) { diff --git a/libs/tools/generator/components/src/generator-services.module.ts b/libs/tools/generator/components/src/generator-services.module.ts index 935f7dc2d60..28e1a325e76 100644 --- a/libs/tools/generator/components/src/generator-services.module.ts +++ b/libs/tools/generator/components/src/generator-services.module.ts @@ -57,7 +57,7 @@ export const SYSTEM_SERVICE_PROVIDER = new SafeInjectionToken { diff --git a/libs/tools/generator/core/src/engine/email-randomizer.spec.ts b/libs/tools/generator/core/src/engine/email-randomizer.spec.ts index 2ebe50d12d4..41ffc8431cd 100644 --- a/libs/tools/generator/core/src/engine/email-randomizer.spec.ts +++ b/libs/tools/generator/core/src/engine/email-randomizer.spec.ts @@ -1,3 +1,7 @@ +/// SDK/WASM code relies on TextEncoder/TextDecoder being available globally +import { TextEncoder, TextDecoder } from "util"; +Object.assign(global, { TextDecoder, TextEncoder }); + import { mock } from "jest-mock-extended"; import { EFFLongWordList } from "@bitwarden/common/platform/misc/wordlist"; diff --git a/libs/tools/generator/core/src/engine/forwarder-context.spec.ts b/libs/tools/generator/core/src/engine/forwarder-context.spec.ts index 9838dbcdbda..e7d848cfed5 100644 --- a/libs/tools/generator/core/src/engine/forwarder-context.spec.ts +++ b/libs/tools/generator/core/src/engine/forwarder-context.spec.ts @@ -1,3 +1,7 @@ +/// SDK/WASM code relies on TextEncoder/TextDecoder being available globally +import { TextEncoder, TextDecoder } from "util"; +Object.assign(global, { TextDecoder, TextEncoder }); + import { mock } from "jest-mock-extended"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; diff --git a/libs/tools/generator/core/src/engine/index.ts b/libs/tools/generator/core/src/engine/index.ts index f8008a866e4..1e9cdeb2d39 100644 --- a/libs/tools/generator/core/src/engine/index.ts +++ b/libs/tools/generator/core/src/engine/index.ts @@ -1,4 +1,4 @@ -export { KeyServiceRandomizer } from "./key-service-randomizer"; +export { PureCryptoRandomizer } from "./purecrypto-randomizer"; export { ForwarderConfiguration, AccountRequest } from "./forwarder-configuration"; export { ForwarderContext } from "./forwarder-context"; export * from "./settings"; diff --git a/libs/tools/generator/core/src/engine/password-randomizer.spec.ts b/libs/tools/generator/core/src/engine/password-randomizer.spec.ts index 1d9f58fddd7..83216ece3e3 100644 --- a/libs/tools/generator/core/src/engine/password-randomizer.spec.ts +++ b/libs/tools/generator/core/src/engine/password-randomizer.spec.ts @@ -1,3 +1,7 @@ +/// SDK/WASM code relies on TextEncoder/TextDecoder being available globally +import { TextEncoder, TextDecoder } from "util"; +Object.assign(global, { TextDecoder, TextEncoder }); + import { mock } from "jest-mock-extended"; import { EFFLongWordList } from "@bitwarden/common/platform/misc/wordlist"; diff --git a/libs/tools/generator/core/src/engine/key-service-randomizer.spec.ts b/libs/tools/generator/core/src/engine/purecrypto-randomizer.spec.ts similarity index 59% rename from libs/tools/generator/core/src/engine/key-service-randomizer.spec.ts rename to libs/tools/generator/core/src/engine/purecrypto-randomizer.spec.ts index 459a05618f9..cc8d9d86196 100644 --- a/libs/tools/generator/core/src/engine/key-service-randomizer.spec.ts +++ b/libs/tools/generator/core/src/engine/purecrypto-randomizer.spec.ts @@ -1,19 +1,28 @@ -import { mock } from "jest-mock-extended"; +import { PureCryptoRandomizer } from "./purecrypto-randomizer"; -import { KeyService } from "@bitwarden/key-management"; +jest.mock("@bitwarden/sdk-internal", () => ({ + PureCrypto: { + random_number: jest.fn(), + }, +})); -import { KeyServiceRandomizer } from "./key-service-randomizer"; +jest.mock("@bitwarden/common/platform/abstractions/sdk/sdk-load.service", () => ({ + SdkLoadService: { + Ready: Promise.resolve(), + }, +})); -describe("KeyServiceRandomizer", () => { - const keyService = mock(); +const mockRandomNumber = jest.requireMock("@bitwarden/sdk-internal").PureCrypto + .random_number as jest.Mock; +describe("PureCryptoRandomizer", () => { afterEach(() => { jest.resetAllMocks(); }); describe("pick", () => { it.each([[null], [undefined], [[]]])("throws when the list is %p", async (list) => { - const randomizer = new KeyServiceRandomizer(keyService); + const randomizer = new PureCryptoRandomizer(); await expect(() => randomizer.pick(list)).rejects.toBeInstanceOf(Error); @@ -21,8 +30,8 @@ describe("KeyServiceRandomizer", () => { }); it("picks an item from the list", async () => { - const randomizer = new KeyServiceRandomizer(keyService); - keyService.randomNumber.mockResolvedValue(1); + const randomizer = new PureCryptoRandomizer(); + mockRandomNumber.mockReturnValue(1); const result = await randomizer.pick([0, 1]); @@ -32,7 +41,7 @@ describe("KeyServiceRandomizer", () => { describe("pickWord", () => { it.each([[null], [undefined], [[]]])("throws when the list is %p", async (list) => { - const randomizer = new KeyServiceRandomizer(keyService); + const randomizer = new PureCryptoRandomizer(); await expect(() => randomizer.pickWord(list)).rejects.toBeInstanceOf(Error); @@ -40,8 +49,8 @@ describe("KeyServiceRandomizer", () => { }); it("picks a word from the list", async () => { - const randomizer = new KeyServiceRandomizer(keyService); - keyService.randomNumber.mockResolvedValue(1); + const randomizer = new PureCryptoRandomizer(); + mockRandomNumber.mockReturnValue(1); const result = await randomizer.pickWord(["foo", "bar"]); @@ -49,8 +58,8 @@ describe("KeyServiceRandomizer", () => { }); it("capitalizes the word when options.titleCase is true", async () => { - const randomizer = new KeyServiceRandomizer(keyService); - keyService.randomNumber.mockResolvedValue(1); + const randomizer = new PureCryptoRandomizer(); + mockRandomNumber.mockReturnValue(1); const result = await randomizer.pickWord(["foo", "bar"], { titleCase: true }); @@ -58,9 +67,9 @@ describe("KeyServiceRandomizer", () => { }); it("appends a random number when options.number is true", async () => { - const randomizer = new KeyServiceRandomizer(keyService); - keyService.randomNumber.mockResolvedValueOnce(1); - keyService.randomNumber.mockResolvedValueOnce(2); + const randomizer = new PureCryptoRandomizer(); + mockRandomNumber.mockReturnValueOnce(1); + mockRandomNumber.mockReturnValueOnce(2); const result = await randomizer.pickWord(["foo", "bar"], { number: true }); @@ -70,7 +79,7 @@ describe("KeyServiceRandomizer", () => { describe("shuffle", () => { it.each([[null], [undefined], [[]]])("throws when the list is %p", async (list) => { - const randomizer = new KeyServiceRandomizer(keyService); + const randomizer = new PureCryptoRandomizer(); await expect(() => randomizer.shuffle(list)).rejects.toBeInstanceOf(Error); @@ -78,18 +87,18 @@ describe("KeyServiceRandomizer", () => { }); it("returns a copy of the list without shuffling it when theres only one entry", async () => { - const randomizer = new KeyServiceRandomizer(keyService); + const randomizer = new PureCryptoRandomizer(); const result = await randomizer.shuffle(["foo"]); expect(result).toEqual(["foo"]); expect(result).not.toBe(["foo"]); - expect(keyService.randomNumber).not.toHaveBeenCalled(); + expect(mockRandomNumber).not.toHaveBeenCalled(); }); it("shuffles the tail of the list", async () => { - const randomizer = new KeyServiceRandomizer(keyService); - keyService.randomNumber.mockResolvedValueOnce(0); + const randomizer = new PureCryptoRandomizer(); + mockRandomNumber.mockReturnValueOnce(0); const result = await randomizer.shuffle(["bar", "foo"]); @@ -97,9 +106,9 @@ describe("KeyServiceRandomizer", () => { }); it("shuffles the list", async () => { - const randomizer = new KeyServiceRandomizer(keyService); - keyService.randomNumber.mockResolvedValueOnce(0); - keyService.randomNumber.mockResolvedValueOnce(1); + const randomizer = new PureCryptoRandomizer(); + mockRandomNumber.mockReturnValueOnce(0); + mockRandomNumber.mockReturnValueOnce(1); const result = await randomizer.shuffle(["baz", "bar", "foo"]); @@ -107,8 +116,8 @@ describe("KeyServiceRandomizer", () => { }); it("returns the input list when options.copy is false", async () => { - const randomizer = new KeyServiceRandomizer(keyService); - keyService.randomNumber.mockResolvedValueOnce(0); + const randomizer = new PureCryptoRandomizer(); + mockRandomNumber.mockReturnValueOnce(0); const expectedResult = ["foo"]; const result = await randomizer.shuffle(expectedResult, { copy: false }); @@ -119,7 +128,7 @@ describe("KeyServiceRandomizer", () => { describe("chars", () => { it("returns an empty string when the length is 0", async () => { - const randomizer = new KeyServiceRandomizer(keyService); + const randomizer = new PureCryptoRandomizer(); const result = await randomizer.chars(0); @@ -127,8 +136,8 @@ describe("KeyServiceRandomizer", () => { }); it("returns an arbitrary lowercase ascii character", async () => { - const randomizer = new KeyServiceRandomizer(keyService); - keyService.randomNumber.mockResolvedValueOnce(0); + const randomizer = new PureCryptoRandomizer(); + mockRandomNumber.mockReturnValueOnce(0); const result = await randomizer.chars(1); @@ -136,38 +145,38 @@ describe("KeyServiceRandomizer", () => { }); it("returns a number of ascii characters based on the length", async () => { - const randomizer = new KeyServiceRandomizer(keyService); - keyService.randomNumber.mockResolvedValue(0); + const randomizer = new PureCryptoRandomizer(); + mockRandomNumber.mockReturnValue(0); const result = await randomizer.chars(2); expect(result).toEqual("aa"); - expect(keyService.randomNumber).toHaveBeenCalledTimes(2); + expect(mockRandomNumber).toHaveBeenCalledTimes(2); }); it("returns a new random character each time its called", async () => { - const randomizer = new KeyServiceRandomizer(keyService); - keyService.randomNumber.mockResolvedValueOnce(0); - keyService.randomNumber.mockResolvedValueOnce(1); + const randomizer = new PureCryptoRandomizer(); + mockRandomNumber.mockReturnValueOnce(0); + mockRandomNumber.mockReturnValueOnce(1); const resultA = await randomizer.chars(1); const resultB = await randomizer.chars(1); expect(resultA).toEqual("a"); expect(resultB).toEqual("b"); - expect(keyService.randomNumber).toHaveBeenCalledTimes(2); + expect(mockRandomNumber).toHaveBeenCalledTimes(2); }); }); describe("uniform", () => { it("forwards requests to the crypto service", async () => { - const randomizer = new KeyServiceRandomizer(keyService); - keyService.randomNumber.mockResolvedValue(5); + const randomizer = new PureCryptoRandomizer(); + mockRandomNumber.mockReturnValue(5); const result = await randomizer.uniform(0, 5); expect(result).toBe(5); - expect(keyService.randomNumber).toHaveBeenCalledWith(0, 5); + expect(mockRandomNumber).toHaveBeenCalledWith(0, 5); }); }); }); diff --git a/libs/tools/generator/core/src/engine/key-service-randomizer.ts b/libs/tools/generator/core/src/engine/purecrypto-randomizer.ts similarity index 73% rename from libs/tools/generator/core/src/engine/key-service-randomizer.ts rename to libs/tools/generator/core/src/engine/purecrypto-randomizer.ts index 5fc719042b7..bbd43b8a231 100644 --- a/libs/tools/generator/core/src/engine/key-service-randomizer.ts +++ b/libs/tools/generator/core/src/engine/purecrypto-randomizer.ts @@ -1,14 +1,18 @@ -import { KeyService } from "@bitwarden/key-management"; +import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service"; +import { PureCrypto } from "@bitwarden/sdk-internal"; import { Randomizer } from "../abstractions"; import { WordOptions } from "../types"; -/** A randomizer backed by a KeyService. */ -export class KeyServiceRandomizer implements Randomizer { - /** instantiates the type. - * @param keyService generates random numbers +/** + * A randomizer backed by the SDK. + * Note: This should be replaced by higher level functions in the SDK eventually. + **/ +export class PureCryptoRandomizer implements Randomizer { + /** + * instantiates the type. */ - constructor(private keyService: KeyService) {} + constructor() {} async pick(list: Array): Promise { const length = list?.length ?? 0; @@ -28,7 +32,8 @@ export class KeyServiceRandomizer implements Randomizer { } if (options?.number ?? false) { - const num = await this.keyService.randomNumber(1, 9); + await SdkLoadService.Ready; + const num = PureCrypto.random_number(0, 9); word = word + num.toString(); } @@ -63,6 +68,7 @@ export class KeyServiceRandomizer implements Randomizer { } async uniform(min: number, max: number) { - return this.keyService.randomNumber(min, max); + await SdkLoadService.Ready; + return PureCrypto.random_number(min, max); } } diff --git a/libs/tools/generator/core/src/engine/username-randomizer.spec.ts b/libs/tools/generator/core/src/engine/username-randomizer.spec.ts index be0650fe16e..e8e288b74f9 100644 --- a/libs/tools/generator/core/src/engine/username-randomizer.spec.ts +++ b/libs/tools/generator/core/src/engine/username-randomizer.spec.ts @@ -1,3 +1,7 @@ +/// SDK/WASM code relies on TextEncoder/TextDecoder being available globally +import { TextEncoder, TextDecoder } from "util"; +Object.assign(global, { TextDecoder, TextEncoder }); + import { mock } from "jest-mock-extended"; import { EFFLongWordList } from "@bitwarden/common/platform/misc/wordlist"; diff --git a/libs/tools/generator/core/src/factories.ts b/libs/tools/generator/core/src/factories.ts index 479545c78fe..0cd716f28af 100644 --- a/libs/tools/generator/core/src/factories.ts +++ b/libs/tools/generator/core/src/factories.ts @@ -1,11 +1,9 @@ // contains logic that constructs generator services dynamically given // a generator id. -import { KeyService } from "@bitwarden/key-management"; - import { Randomizer } from "./abstractions"; -import { KeyServiceRandomizer } from "./engine/key-service-randomizer"; +import { PureCryptoRandomizer } from "./engine/purecrypto-randomizer"; -export function createRandomizer(keyService: KeyService): Randomizer { - return new KeyServiceRandomizer(keyService); +export function createRandomizer(): Randomizer { + return new PureCryptoRandomizer(); } diff --git a/libs/tools/generator/core/src/metadata/email/catchall.spec.ts b/libs/tools/generator/core/src/metadata/email/catchall.spec.ts index 1099a6d59ea..4074b4639b6 100644 --- a/libs/tools/generator/core/src/metadata/email/catchall.spec.ts +++ b/libs/tools/generator/core/src/metadata/email/catchall.spec.ts @@ -1,3 +1,7 @@ +/// SDK/WASM code relies on TextEncoder/TextDecoder being available globally +import { TextEncoder, TextDecoder } from "util"; +Object.assign(global, { TextDecoder, TextEncoder }); + import { mock } from "jest-mock-extended"; import { EmailRandomizer } from "../../engine"; diff --git a/libs/tools/generator/core/src/metadata/email/plus-address.spec.ts b/libs/tools/generator/core/src/metadata/email/plus-address.spec.ts index befc900ceab..78054133020 100644 --- a/libs/tools/generator/core/src/metadata/email/plus-address.spec.ts +++ b/libs/tools/generator/core/src/metadata/email/plus-address.spec.ts @@ -1,3 +1,7 @@ +/// SDK/WASM code relies on TextEncoder/TextDecoder being available globally +import { TextEncoder, TextDecoder } from "util"; +Object.assign(global, { TextDecoder, TextEncoder }); + import { mock } from "jest-mock-extended"; import { EmailRandomizer } from "../../engine"; diff --git a/libs/tools/generator/core/src/metadata/password/eff-word-list.spec.ts b/libs/tools/generator/core/src/metadata/password/eff-word-list.spec.ts index bdf021c50f3..aacb82a51c4 100644 --- a/libs/tools/generator/core/src/metadata/password/eff-word-list.spec.ts +++ b/libs/tools/generator/core/src/metadata/password/eff-word-list.spec.ts @@ -1,3 +1,7 @@ +/// SDK/WASM code relies on TextEncoder/TextDecoder being available globally +import { TextEncoder, TextDecoder } from "util"; +Object.assign(global, { TextDecoder, TextEncoder }); + import { mock } from "jest-mock-extended"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; diff --git a/libs/tools/generator/core/src/metadata/password/random-password.spec.ts b/libs/tools/generator/core/src/metadata/password/random-password.spec.ts index 9efd5350c21..743a8d53a2b 100644 --- a/libs/tools/generator/core/src/metadata/password/random-password.spec.ts +++ b/libs/tools/generator/core/src/metadata/password/random-password.spec.ts @@ -1,3 +1,7 @@ +/// SDK/WASM code relies on TextEncoder/TextDecoder being available globally +import { TextEncoder, TextDecoder } from "util"; +Object.assign(global, { TextDecoder, TextEncoder }); + import { mock } from "jest-mock-extended"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; diff --git a/libs/tools/generator/core/src/metadata/username/eff-word-list.spec.ts b/libs/tools/generator/core/src/metadata/username/eff-word-list.spec.ts index beebb016504..0f6653360db 100644 --- a/libs/tools/generator/core/src/metadata/username/eff-word-list.spec.ts +++ b/libs/tools/generator/core/src/metadata/username/eff-word-list.spec.ts @@ -1,3 +1,7 @@ +/// SDK/WASM code relies on TextEncoder/TextDecoder being available globally +import { TextEncoder, TextDecoder } from "util"; +Object.assign(global, { TextDecoder, TextEncoder }); + import { mock } from "jest-mock-extended"; import { IdentityConstraint } from "@bitwarden/common/tools/state/identity-state-constraint"; diff --git a/libs/tools/generator/core/src/policies/available-algorithms-policy.spec.ts b/libs/tools/generator/core/src/policies/available-algorithms-policy.spec.ts index 7de8c708dcf..085ea72e5d5 100644 --- a/libs/tools/generator/core/src/policies/available-algorithms-policy.spec.ts +++ b/libs/tools/generator/core/src/policies/available-algorithms-policy.spec.ts @@ -1,3 +1,7 @@ +/// SDK/WASM code relies on TextEncoder/TextDecoder being available globally +import { TextEncoder, TextDecoder } from "util"; +Object.assign(global, { TextDecoder, TextEncoder }); + import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; import { PolicyId } from "@bitwarden/common/types/guid"; diff --git a/libs/tools/generator/core/src/policies/dynamic-password-policy-constraints.spec.ts b/libs/tools/generator/core/src/policies/dynamic-password-policy-constraints.spec.ts index 0bebb0825bf..4d4d54a6e18 100644 --- a/libs/tools/generator/core/src/policies/dynamic-password-policy-constraints.spec.ts +++ b/libs/tools/generator/core/src/policies/dynamic-password-policy-constraints.spec.ts @@ -1,3 +1,7 @@ +/// SDK/WASM code relies on TextEncoder/TextDecoder being available globally +import { TextEncoder, TextDecoder } from "util"; +Object.assign(global, { TextDecoder, TextEncoder }); + import { ObjectKey } from "@bitwarden/common/tools/state/object-key"; import { BuiltIn, Profile } from "../metadata"; diff --git a/libs/tools/generator/core/src/policies/passphrase-policy-constraints.spec.ts b/libs/tools/generator/core/src/policies/passphrase-policy-constraints.spec.ts index 6306382c84e..c640b05da8e 100644 --- a/libs/tools/generator/core/src/policies/passphrase-policy-constraints.spec.ts +++ b/libs/tools/generator/core/src/policies/passphrase-policy-constraints.spec.ts @@ -1,3 +1,7 @@ +/// SDK/WASM code relies on TextEncoder/TextDecoder being available globally +import { TextEncoder, TextDecoder } from "util"; +Object.assign(global, { TextDecoder, TextEncoder }); + import { BuiltIn, Profile } from "../metadata"; import { PassphrasePolicyConstraints } from "./passphrase-policy-constraints"; diff --git a/libs/tools/generator/core/src/providers/credential-preferences.spec.ts b/libs/tools/generator/core/src/providers/credential-preferences.spec.ts index 6fd747f3823..cb1ed620c5b 100644 --- a/libs/tools/generator/core/src/providers/credential-preferences.spec.ts +++ b/libs/tools/generator/core/src/providers/credential-preferences.spec.ts @@ -1,3 +1,7 @@ +/// SDK/WASM code relies on TextEncoder/TextDecoder being available globally +import { TextEncoder, TextDecoder } from "util"; +Object.assign(global, { TextDecoder, TextEncoder }); + import { AlgorithmsByType, Type } from "../metadata"; import { CredentialPreference } from "../types"; diff --git a/libs/tools/generator/core/src/providers/generator-metadata-provider.spec.ts b/libs/tools/generator/core/src/providers/generator-metadata-provider.spec.ts index 39ff74ad901..58b7c1f6804 100644 --- a/libs/tools/generator/core/src/providers/generator-metadata-provider.spec.ts +++ b/libs/tools/generator/core/src/providers/generator-metadata-provider.spec.ts @@ -1,3 +1,7 @@ +/// SDK/WASM code relies on TextEncoder/TextDecoder being available globally +import { TextEncoder, TextDecoder } from "util"; +Object.assign(global, { TextDecoder, TextEncoder }); + import { mock } from "jest-mock-extended"; import { BehaviorSubject, ReplaySubject, firstValueFrom } from "rxjs"; diff --git a/libs/tools/generator/core/src/services/default-credential-generator.service.spec.ts b/libs/tools/generator/core/src/services/default-credential-generator.service.spec.ts index e459bb47f47..5c8389b7377 100644 --- a/libs/tools/generator/core/src/services/default-credential-generator.service.spec.ts +++ b/libs/tools/generator/core/src/services/default-credential-generator.service.spec.ts @@ -1,3 +1,7 @@ +/// SDK/WASM code relies on TextEncoder/TextDecoder being available globally +import { TextEncoder, TextDecoder } from "util"; +Object.assign(global, { TextDecoder, TextEncoder }); + import { BehaviorSubject, Subject, firstValueFrom, of } from "rxjs"; import { Account } from "@bitwarden/common/auth/abstractions/account.service"; diff --git a/libs/tools/generator/core/src/strategies/catchall-generator-strategy.spec.ts b/libs/tools/generator/core/src/strategies/catchall-generator-strategy.spec.ts index 99f618a4520..2de938c13dc 100644 --- a/libs/tools/generator/core/src/strategies/catchall-generator-strategy.spec.ts +++ b/libs/tools/generator/core/src/strategies/catchall-generator-strategy.spec.ts @@ -1,3 +1,7 @@ +/// SDK/WASM code relies on TextEncoder/TextDecoder being available globally +import { TextEncoder, TextDecoder } from "util"; +Object.assign(global, { TextDecoder, TextEncoder }); + import { mock } from "jest-mock-extended"; import { of, firstValueFrom } from "rxjs"; diff --git a/libs/tools/generator/core/src/strategies/eff-username-generator-strategy.spec.ts b/libs/tools/generator/core/src/strategies/eff-username-generator-strategy.spec.ts index d3582127ade..ba98eb75e23 100644 --- a/libs/tools/generator/core/src/strategies/eff-username-generator-strategy.spec.ts +++ b/libs/tools/generator/core/src/strategies/eff-username-generator-strategy.spec.ts @@ -1,3 +1,7 @@ +/// SDK/WASM code relies on TextEncoder/TextDecoder being available globally +import { TextEncoder, TextDecoder } from "util"; +Object.assign(global, { TextDecoder, TextEncoder }); + import { mock } from "jest-mock-extended"; import { of, firstValueFrom } from "rxjs"; diff --git a/libs/tools/generator/core/src/strategies/forwarder-generator-strategy.spec.ts b/libs/tools/generator/core/src/strategies/forwarder-generator-strategy.spec.ts index 99834a25417..3038d9bbdf6 100644 --- a/libs/tools/generator/core/src/strategies/forwarder-generator-strategy.spec.ts +++ b/libs/tools/generator/core/src/strategies/forwarder-generator-strategy.spec.ts @@ -1,3 +1,7 @@ +/// SDK/WASM code relies on TextEncoder/TextDecoder being available globally +import { TextEncoder, TextDecoder } from "util"; +Object.assign(global, { TextDecoder, TextEncoder }); + import { mock } from "jest-mock-extended"; import { of, firstValueFrom } from "rxjs"; diff --git a/libs/tools/generator/core/src/strategies/passphrase-generator-strategy.spec.ts b/libs/tools/generator/core/src/strategies/passphrase-generator-strategy.spec.ts index ee521d753ae..9c9bf92aa64 100644 --- a/libs/tools/generator/core/src/strategies/passphrase-generator-strategy.spec.ts +++ b/libs/tools/generator/core/src/strategies/passphrase-generator-strategy.spec.ts @@ -1,3 +1,7 @@ +/// SDK/WASM code relies on TextEncoder/TextDecoder being available globally +import { TextEncoder, TextDecoder } from "util"; +Object.assign(global, { TextDecoder, TextEncoder }); + import { mock } from "jest-mock-extended"; import { of, firstValueFrom } from "rxjs"; diff --git a/libs/tools/generator/core/src/strategies/password-generator-strategy.spec.ts b/libs/tools/generator/core/src/strategies/password-generator-strategy.spec.ts index 94e7c16be28..52d0d4fa272 100644 --- a/libs/tools/generator/core/src/strategies/password-generator-strategy.spec.ts +++ b/libs/tools/generator/core/src/strategies/password-generator-strategy.spec.ts @@ -1,3 +1,7 @@ +/// SDK/WASM code relies on TextEncoder/TextDecoder being available globally +import { TextEncoder, TextDecoder } from "util"; +Object.assign(global, { TextDecoder, TextEncoder }); + import { mock } from "jest-mock-extended"; import { of, firstValueFrom } from "rxjs"; diff --git a/libs/tools/generator/core/src/strategies/subaddress-generator-strategy.spec.ts b/libs/tools/generator/core/src/strategies/subaddress-generator-strategy.spec.ts index 0be5132c67f..fdd08dccb6b 100644 --- a/libs/tools/generator/core/src/strategies/subaddress-generator-strategy.spec.ts +++ b/libs/tools/generator/core/src/strategies/subaddress-generator-strategy.spec.ts @@ -1,3 +1,7 @@ +/// SDK/WASM code relies on TextEncoder/TextDecoder being available globally +import { TextEncoder, TextDecoder } from "util"; +Object.assign(global, { TextDecoder, TextEncoder }); + import { mock } from "jest-mock-extended"; import { of, firstValueFrom } from "rxjs"; diff --git a/libs/tools/generator/core/src/types/generated-credential.spec.ts b/libs/tools/generator/core/src/types/generated-credential.spec.ts index 3d8d2c9bd4d..bb4cce34052 100644 --- a/libs/tools/generator/core/src/types/generated-credential.spec.ts +++ b/libs/tools/generator/core/src/types/generated-credential.spec.ts @@ -1,3 +1,7 @@ +/// SDK/WASM code relies on TextEncoder/TextDecoder being available globally +import { TextEncoder, TextDecoder } from "util"; +Object.assign(global, { TextDecoder, TextEncoder }); + import { Type } from "../metadata"; import { GeneratedCredential } from "./generated-credential"; diff --git a/libs/tools/generator/extensions/history/src/generated-credential.spec.ts b/libs/tools/generator/extensions/history/src/generated-credential.spec.ts index 26a48cb83ea..3d3bd43126a 100644 --- a/libs/tools/generator/extensions/history/src/generated-credential.spec.ts +++ b/libs/tools/generator/extensions/history/src/generated-credential.spec.ts @@ -1,3 +1,7 @@ +/// SDK/WASM code relies on TextEncoder/TextDecoder being available globally +import { TextEncoder, TextDecoder } from "util"; +Object.assign(global, { TextDecoder, TextEncoder }); + import { Type } from "@bitwarden/generator-core"; import { GeneratedCredential } from "."; diff --git a/libs/tools/generator/extensions/history/src/local-generator-history.service.spec.ts b/libs/tools/generator/extensions/history/src/local-generator-history.service.spec.ts index 81335887f0d..5724b201f30 100644 --- a/libs/tools/generator/extensions/history/src/local-generator-history.service.spec.ts +++ b/libs/tools/generator/extensions/history/src/local-generator-history.service.spec.ts @@ -1,3 +1,7 @@ +/// SDK/WASM code relies on TextEncoder/TextDecoder being available globally +import { TextEncoder, TextDecoder } from "util"; +Object.assign(global, { TextDecoder, TextEncoder }); + import { mock } from "jest-mock-extended"; import { firstValueFrom, of } from "rxjs"; diff --git a/libs/tools/generator/extensions/legacy/src/create-legacy-password-generation-service.ts b/libs/tools/generator/extensions/legacy/src/create-legacy-password-generation-service.ts index 0048ce15499..6fbdf4afbed 100644 --- a/libs/tools/generator/extensions/legacy/src/create-legacy-password-generation-service.ts +++ b/libs/tools/generator/extensions/legacy/src/create-legacy-password-generation-service.ts @@ -13,7 +13,7 @@ import { LegacyPasswordGenerationService } from "./legacy-password-generation.se import { PasswordGenerationServiceAbstraction } from "./password-generation.service.abstraction"; const { PassphraseGeneratorStrategy, PasswordGeneratorStrategy } = strategies; -const { KeyServiceRandomizer, PasswordRandomizer } = engine; +const { PureCryptoRandomizer, PasswordRandomizer } = engine; const DefaultGeneratorService = services.DefaultGeneratorService; @@ -24,7 +24,7 @@ export function legacyPasswordGenerationServiceFactory( accountService: AccountService, stateProvider: StateProvider, ): PasswordGenerationServiceAbstraction { - const randomizer = new KeyServiceRandomizer(keyService); + const randomizer = new PureCryptoRandomizer(); const passwordRandomizer = new PasswordRandomizer(randomizer, Date.now); const passwords = new DefaultGeneratorService( diff --git a/libs/tools/generator/extensions/legacy/src/create-legacy-username-generation-service.ts b/libs/tools/generator/extensions/legacy/src/create-legacy-username-generation-service.ts index 36b4a20aec7..2507c5f7ecd 100644 --- a/libs/tools/generator/extensions/legacy/src/create-legacy-username-generation-service.ts +++ b/libs/tools/generator/extensions/legacy/src/create-legacy-username-generation-service.ts @@ -14,7 +14,7 @@ import { KeyService } from "@bitwarden/key-management"; import { LegacyUsernameGenerationService } from "./legacy-username-generation.service"; import { UsernameGenerationServiceAbstraction } from "./username-generation.service.abstraction"; -const { KeyServiceRandomizer, UsernameRandomizer, EmailRandomizer, EmailCalculator } = engine; +const { PureCryptoRandomizer, UsernameRandomizer, EmailRandomizer, EmailCalculator } = engine; const DefaultGeneratorService = services.DefaultGeneratorService; const { CatchallGeneratorStrategy, @@ -32,7 +32,7 @@ export function legacyUsernameGenerationServiceFactory( accountService: AccountService, stateProvider: StateProvider, ): UsernameGenerationServiceAbstraction { - const randomizer = new KeyServiceRandomizer(keyService); + const randomizer = new PureCryptoRandomizer(); const restClient = new RestClient(apiService, i18nService); const usernameRandomizer = new UsernameRandomizer(randomizer); const emailRandomizer = new EmailRandomizer(randomizer); diff --git a/libs/tools/generator/extensions/legacy/src/legacy-password-generation.service.spec.ts b/libs/tools/generator/extensions/legacy/src/legacy-password-generation.service.spec.ts index f575cd3b619..7846d78c77b 100644 --- a/libs/tools/generator/extensions/legacy/src/legacy-password-generation.service.spec.ts +++ b/libs/tools/generator/extensions/legacy/src/legacy-password-generation.service.spec.ts @@ -1,3 +1,7 @@ +/// SDK/WASM code relies on TextEncoder/TextDecoder being available globally +import { TextEncoder, TextDecoder } from "util"; +Object.assign(global, { TextDecoder, TextEncoder }); + import { mock } from "jest-mock-extended"; import { of } from "rxjs"; diff --git a/libs/tools/generator/extensions/legacy/src/legacy-username-generation.service.spec.ts b/libs/tools/generator/extensions/legacy/src/legacy-username-generation.service.spec.ts index 5a4dce4f4a5..de2059f86ef 100644 --- a/libs/tools/generator/extensions/legacy/src/legacy-username-generation.service.spec.ts +++ b/libs/tools/generator/extensions/legacy/src/legacy-username-generation.service.spec.ts @@ -1,3 +1,7 @@ +/// SDK/WASM code relies on TextEncoder/TextDecoder being available globally +import { TextEncoder, TextDecoder } from "util"; +Object.assign(global, { TextDecoder, TextEncoder }); + // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { mock } from "jest-mock-extended"; diff --git a/libs/tools/generator/extensions/navigation/src/default-generator-navigation.service.spec.ts b/libs/tools/generator/extensions/navigation/src/default-generator-navigation.service.spec.ts index 37e8ec6e379..cd98ea30ace 100644 --- a/libs/tools/generator/extensions/navigation/src/default-generator-navigation.service.spec.ts +++ b/libs/tools/generator/extensions/navigation/src/default-generator-navigation.service.spec.ts @@ -1,3 +1,7 @@ +/// SDK/WASM code relies on TextEncoder/TextDecoder being available globally +import { TextEncoder, TextDecoder } from "util"; +Object.assign(global, { TextDecoder, TextEncoder }); + import { mock } from "jest-mock-extended"; import { firstValueFrom, of } from "rxjs"; diff --git a/libs/tools/generator/extensions/navigation/src/generator-navigation-evaluator.spec.ts b/libs/tools/generator/extensions/navigation/src/generator-navigation-evaluator.spec.ts index 82b9e29e91a..999edb2d25c 100644 --- a/libs/tools/generator/extensions/navigation/src/generator-navigation-evaluator.spec.ts +++ b/libs/tools/generator/extensions/navigation/src/generator-navigation-evaluator.spec.ts @@ -1,3 +1,7 @@ +/// SDK/WASM code relies on TextEncoder/TextDecoder being available globally +import { TextEncoder, TextDecoder } from "util"; +Object.assign(global, { TextDecoder, TextEncoder }); + import { DefaultGeneratorNavigation } from "./default-generator-navigation"; import { GeneratorNavigationEvaluator } from "./generator-navigation-evaluator"; From 3a56f2e832ac373a038408466690449a77b3ea58 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Fri, 20 Feb 2026 19:22:05 +0100 Subject: [PATCH 54/89] [PM-30785|BEEEP] Remove deprecated master key login with device flow (#17943) * Remove deprecated master key login with device flow * Resolve conflicts / cleanup * Linting * Fix lint * Run prettier --- .../angular/login-via-auth-request/README.md | 31 ++------ .../login-via-auth-request.component.ts | 77 ++++--------------- .../auth-request.service.abstraction.ts | 25 +----- .../auth-request-login.strategy.spec.ts | 45 +---------- .../auth-request-login.strategy.ts | 15 +--- .../sso-login.strategy.spec.ts | 21 ----- .../login-strategies/sso-login.strategy.ts | 22 ++---- .../common/models/domain/login-credentials.ts | 7 +- .../auth-request/auth-request.service.spec.ts | 56 +------------- .../auth-request/auth-request.service.ts | 48 +----------- .../login-strategy.state.spec.ts | 2 - .../models/response/auth-request.response.ts | 4 +- .../crypto/abstractions/encrypt.service.ts | 7 -- .../encrypt.service.implementation.ts | 20 ----- 14 files changed, 34 insertions(+), 346 deletions(-) diff --git a/libs/auth/src/angular/login-via-auth-request/README.md b/libs/auth/src/angular/login-via-auth-request/README.md index 3396ba8698b..d1c4ddf3f0a 100644 --- a/libs/auth/src/angular/login-via-auth-request/README.md +++ b/libs/auth/src/angular/login-via-auth-request/README.md @@ -13,16 +13,9 @@ ## Standard Auth Request Flows -### Flow 1: Unauthed user requests approval from device; Approving device has a masterKey in memory +### Flow 1: This flow was removed -1. Unauthed user clicks "Login with device" -2. Navigates to `/login-with-device` which creates a `StandardAuthRequest` -3. Receives approval from a device with authRequestPublicKey(masterKey) -4. Decrypts masterKey -5. Decrypts userKey -6. Proceeds to vault - -### Flow 2: Unauthed user requests approval from device; Approving device does NOT have a masterKey in memory +### Flow 2: Unauthed user requests approval from device; Approving device does NOT need to have a masterKey in memory 1. Unauthed user clicks "Login with device" 2. Navigates to `/login-with-device` which creates a `StandardAuthRequest` @@ -33,28 +26,18 @@ **Note:** This flow is an uncommon scenario and relates to TDE off-boarding. The following describes how a user could get into this flow: -1. An SSO TD user logs into a device via an Admin auth request approval, therefore this device does NOT have a masterKey +1. An SSO TD user logs into a device via an Admin auth request approval, therefore this device does NOT need to have a masterKey in memory 2. The org admin: - Changes the member decryption options from "Trusted devices" to "Master password" AND - Turns off the "Require single sign-on authentication" policy 3. On another device, the user clicks "Login with device", which they can do because the org no longer requires SSO -4. The user approves from the device they had previously logged into with SSO TD, which does NOT have a masterKey in +4. The user approves from the device they had previously logged into with SSO TD, which does NOT need to have a masterKey in memory -### Flow 3: Authed SSO TD user requests approval from device; Approving device has a masterKey in memory +### Flow 3: This flow was removed -1. SSO TD user authenticates via SSO -2. Navigates to `/login-initiated` -3. Clicks "Approve from your other device" -4. Navigates to `/login-with-device` which creates a `StandardAuthRequest` -5. Receives approval from device with authRequestPublicKey(masterKey) -6. Decrypts masterKey -7. Decrypts userKey -8. Establishes trust (if required) -9. Proceeds to vault - -### Flow 4: Authed SSO TD user requests approval from device; Approving device does NOT have a masterKey in memory +### Flow 4: Authed SSO TD user requests approval from device; Approving device does NOT need to have a masterKey in memory 1. SSO TD user authenticates via SSO 2. Navigates to `/login-initiated` @@ -89,9 +72,7 @@ userKey. This is how admins are able to send over the authRequestPublicKey(userK | Flow | Auth Status | Clicks Button [active route] | Navigates to | Approving device has masterKey in memory\* | | --------------- | ----------- | ----------------------------------------------------- | --------------------------- | ------------------------------------------------- | -| Standard Flow 1 | unauthed | "Login with device" [`/login`] | `/login-with-device` | yes | | Standard Flow 2 | unauthed | "Login with device" [`/login`] | `/login-with-device` | no | -| Standard Flow 3 | authed | "Approve from your other device" [`/login-initiated`] | `/login-with-device` | yes | | Standard Flow 4 | authed | "Approve from your other device" [`/login-initiated`] | `/login-with-device` | no | | Admin Flow | authed | "Request admin approval"
[`/login-initiated`] | `/admin-approval-requested` | NA - admin requests always send encrypted userKey | diff --git a/libs/auth/src/angular/login-via-auth-request/login-via-auth-request.component.ts b/libs/auth/src/angular/login-via-auth-request/login-via-auth-request.component.ts index fc91f220138..c7046d39022 100644 --- a/libs/auth/src/angular/login-via-auth-request/login-via-auth-request.component.ts +++ b/libs/auth/src/angular/login-via-auth-request/login-via-auth-request.component.ts @@ -605,10 +605,10 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy { if (authRequestResponse.requestApproved) { const userHasAuthenticatedViaSSO = this.authStatus === AuthenticationStatus.Locked; if (userHasAuthenticatedViaSSO) { - // [Standard Flow 3-4] Handle authenticated SSO TD user flows + // [Standard Flow 4] Handle authenticated SSO TD user flows return await this.handleAuthenticatedFlows(authRequestResponse); } else { - // [Standard Flow 1-2] Handle unauthenticated user flows + // [Standard Flow 2] Handle unauthenticated user flows return await this.handleUnauthenticatedFlows(authRequestResponse, requestId); } } @@ -629,7 +629,7 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy { } private async handleAuthenticatedFlows(authRequestResponse: AuthRequestResponse) { - // [Standard Flow 3-4] Handle authenticated SSO TD user flows + // [Standard Flow 4] Handle authenticated SSO TD user flows const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; if (!userId) { this.logService.error( @@ -654,7 +654,7 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy { authRequestResponse: AuthRequestResponse, requestId: string, ) { - // [Standard Flow 1-2] Handle unauthenticated user flows + // [Standard Flow 2] Handle unauthenticated user flows const authRequestLoginCredentials = await this.buildAuthRequestLoginCredentials( requestId, authRequestResponse, @@ -679,27 +679,12 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy { privateKey: Uint8Array, userId: UserId, ): Promise { - /** - * [Flow Type Detection] - * We determine the type of `key` based on the presence or absence of `masterPasswordHash`: - * - If `masterPasswordHash` exists: Standard Flow 1 or 3 (device has masterKey) - * - If no `masterPasswordHash`: Standard Flow 2, 4, or Admin Flow (device sends userKey) - */ - if (authRequestResponse.masterPasswordHash) { - // [Standard Flow 1 or 3] Device has masterKey - await this.authRequestService.setKeysAfterDecryptingSharedMasterKeyAndHash( - authRequestResponse, - privateKey, - userId, - ); - } else { - // [Standard Flow 2, 4, or Admin Flow] Device sends userKey - await this.authRequestService.setUserKeyAfterDecryptingSharedUserKey( - authRequestResponse, - privateKey, - userId, - ); - } + // [Standard Flow 2, 4, or Admin Flow] Device sends userKey + await this.authRequestService.setUserKeyAfterDecryptingSharedUserKey( + authRequestResponse, + privateKey, + userId, + ); // [Admin Flow Cleanup] Clear one-time use admin auth request // clear the admin auth request from state so it cannot be used again (it's a one time use) @@ -758,43 +743,13 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy { /** * See verifyAndHandleApprovedAuthReq() for flow details. - * - * We determine the type of `key` based on the presence or absence of `masterPasswordHash`: - * - If `masterPasswordHash` has a value, we receive the `key` as an authRequestPublicKey(masterKey) [plus we have authRequestPublicKey(masterPasswordHash)] - * - If `masterPasswordHash` does not have a value, we receive the `key` as an authRequestPublicKey(userKey) */ - if (authRequestResponse.masterPasswordHash) { - // ...in Standard Auth Request Flow 1 - const { masterKey, masterKeyHash } = - await this.authRequestService.decryptPubKeyEncryptedMasterKeyAndHash( - authRequestResponse.key, - authRequestResponse.masterPasswordHash, - this.authRequestKeyPair.privateKey, - ); - - return new AuthRequestLoginCredentials( - this.email, - this.accessCode, - requestId, - null, // no userKey - masterKey, - masterKeyHash, - ); - } else { - // ...in Standard Auth Request Flow 2 - const userKey = await this.authRequestService.decryptPubKeyEncryptedUserKey( - authRequestResponse.key, - this.authRequestKeyPair.privateKey, - ); - return new AuthRequestLoginCredentials( - this.email, - this.accessCode, - requestId, - userKey, - null, // no masterKey - null, // no masterKeyHash - ); - } + // ...in Standard Auth Request Flow 2 + const userKey = await this.authRequestService.decryptPubKeyEncryptedUserKey( + authRequestResponse.key, + this.authRequestKeyPair.privateKey, + ); + return new AuthRequestLoginCredentials(this.email, this.accessCode, requestId, userKey); } private async clearExistingAdminAuthRequestAndStartNewRequest(userId: UserId) { diff --git a/libs/auth/src/common/abstractions/auth-request.service.abstraction.ts b/libs/auth/src/common/abstractions/auth-request.service.abstraction.ts index 1077bc024e9..04128768759 100644 --- a/libs/auth/src/common/abstractions/auth-request.service.abstraction.ts +++ b/libs/auth/src/common/abstractions/auth-request.service.abstraction.ts @@ -4,7 +4,7 @@ import { AdminAuthRequestStorable } from "@bitwarden/common/auth/models/domain/a import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response"; import { AuthRequestPushNotification } from "@bitwarden/common/models/response/notification.response"; import { UserId } from "@bitwarden/common/types/guid"; -import { UserKey, MasterKey } from "@bitwarden/common/types/key"; +import { UserKey } from "@bitwarden/common/types/key"; export abstract class AuthRequestServiceAbstraction { /** Emits an auth request id when an auth request has been approved. */ @@ -75,17 +75,6 @@ export abstract class AuthRequestServiceAbstraction { authReqPrivateKey: Uint8Array, userId: UserId, ): Promise; - /** - * Sets the `MasterKey` and `MasterKeyHash` from an auth request. Auth request must have a `MasterKey` and `MasterKeyHash`. - * @param authReqResponse The auth request. - * @param authReqPrivateKey The private key corresponding to the public key sent in the auth request. - * @param userId The ID of the user for whose account we will set the keys. - */ - abstract setKeysAfterDecryptingSharedMasterKeyAndHash( - authReqResponse: AuthRequestResponse, - authReqPrivateKey: Uint8Array, - userId: UserId, - ): Promise; /** * Decrypts a `UserKey` from a public key encrypted `UserKey`. * @param pubKeyEncryptedUserKey The public key encrypted `UserKey`. @@ -96,18 +85,6 @@ export abstract class AuthRequestServiceAbstraction { pubKeyEncryptedUserKey: string, privateKey: Uint8Array, ): Promise; - /** - * Decrypts a `MasterKey` and `MasterKeyHash` from a public key encrypted `MasterKey` and `MasterKeyHash`. - * @param pubKeyEncryptedMasterKey The public key encrypted `MasterKey`. - * @param pubKeyEncryptedMasterKeyHash The public key encrypted `MasterKeyHash`. - * @param privateKey The private key corresponding to the public key used to encrypt the `MasterKey` and `MasterKeyHash`. - * @returns The decrypted `MasterKey` and `MasterKeyHash`. - */ - abstract decryptPubKeyEncryptedMasterKeyAndHash( - pubKeyEncryptedMasterKey: string, - pubKeyEncryptedMasterKeyHash: string, - privateKey: Uint8Array, - ): Promise<{ masterKey: MasterKey; masterKeyHash: string }>; /** * Handles incoming auth request push server notifications. diff --git a/libs/auth/src/common/login-strategies/auth-request-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/auth-request-login.strategy.spec.ts index 275f2d97aa4..b07dc1202de 100644 --- a/libs/auth/src/common/login-strategies/auth-request-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/auth-request-login.strategy.spec.ts @@ -26,7 +26,7 @@ import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/sym import { makeEncString, FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; import { CsprngArray } from "@bitwarden/common/types/csprng"; import { UserId } from "@bitwarden/common/types/guid"; -import { MasterKey, UserKey } from "@bitwarden/common/types/key"; +import { UserKey } from "@bitwarden/common/types/key"; import { KdfConfigService, KeyService } from "@bitwarden/key-management"; import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction"; @@ -73,11 +73,7 @@ describe("AuthRequestLoginStrategy", () => { const email = "EMAIL"; const accessCode = "ACCESS_CODE"; const authRequestId = "AUTH_REQUEST_ID"; - const decMasterKey = new SymmetricCryptoKey( - new Uint8Array(64).buffer as CsprngArray, - ) as MasterKey; const decUserKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as UserKey; - const decMasterKeyHash = "LOCAL_PASSWORD_HASH"; beforeEach(async () => { keyService = mock(); @@ -150,42 +146,6 @@ describe("AuthRequestLoginStrategy", () => { ); }); - it("sets keys after a successful authentication when masterKey and masterKeyHash provided in login credentials", async () => { - credentials = new AuthRequestLoginCredentials( - email, - accessCode, - authRequestId, - null, - decMasterKey, - decMasterKeyHash, - ); - - const masterKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as MasterKey; - const userKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as UserKey; - - masterPasswordService.masterKeySubject.next(masterKey); - masterPasswordService.mock.decryptUserKeyWithMasterKey.mockResolvedValue(userKey); - tokenService.decodeAccessToken.mockResolvedValue({ sub: mockUserId }); - - await authRequestLoginStrategy.logIn(credentials); - - expect(masterPasswordService.mock.setMasterKey).toHaveBeenCalledWith(masterKey, mockUserId); - expect(masterPasswordService.mock.setMasterKeyHash).toHaveBeenCalledWith( - decMasterKeyHash, - mockUserId, - ); - expect(masterPasswordService.mock.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith( - tokenResponse.key, - mockUserId, - ); - expect(keyService.setUserKey).toHaveBeenCalledWith(userKey, mockUserId); - expect(deviceTrustService.trustDeviceIfRequired).toHaveBeenCalled(); - expect(accountCryptographicStateService.setAccountCryptographicState).toHaveBeenCalledWith( - { V1: { private_key: tokenResponse.privateKey } }, - mockUserId, - ); - }); - it("sets keys after a successful authentication when only userKey provided in login credentials", async () => { // Initialize credentials with only userKey credentials = new AuthRequestLoginCredentials( @@ -193,8 +153,6 @@ describe("AuthRequestLoginStrategy", () => { accessCode, authRequestId, decUserKey, // Pass userKey - null, // No masterKey - null, // No masterKeyHash ); // Call logIn @@ -240,7 +198,6 @@ describe("AuthRequestLoginStrategy", () => { }; apiService.postIdentityToken.mockResolvedValue(tokenResponse); - masterPasswordService.masterKeySubject.next(decMasterKey); masterPasswordService.mock.decryptUserKeyWithMasterKey.mockResolvedValue(decUserKey); await authRequestLoginStrategy.logIn(credentials); diff --git a/libs/auth/src/common/login-strategies/auth-request-login.strategy.ts b/libs/auth/src/common/login-strategies/auth-request-login.strategy.ts index 66b9ee83919..1f3eaf7c164 100644 --- a/libs/auth/src/common/login-strategies/auth-request-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/auth-request-login.strategy.ts @@ -72,20 +72,7 @@ export class AuthRequestLoginStrategy extends LoginStrategy { } protected override async setMasterKey(response: IdentityTokenResponse, userId: UserId) { - const authRequestCredentials = this.cache.value.authRequestCredentials; - if ( - authRequestCredentials.decryptedMasterKey && - authRequestCredentials.decryptedMasterKeyHash - ) { - await this.masterPasswordService.setMasterKey( - authRequestCredentials.decryptedMasterKey, - userId, - ); - await this.masterPasswordService.setMasterKeyHash( - authRequestCredentials.decryptedMasterKeyHash, - userId, - ); - } + // This login strategy does not use a master key } protected override async setUserKey( diff --git a/libs/auth/src/common/login-strategies/sso-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/sso-login.strategy.spec.ts index 4f0a6bbf73f..f7a97baba0a 100644 --- a/libs/auth/src/common/login-strategies/sso-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/sso-login.strategy.spec.ts @@ -416,24 +416,6 @@ describe("SsoLoginStrategy", () => { ); }); - it("sets the user key using master key and hash from approved admin request if exists", async () => { - apiService.postIdentityToken.mockResolvedValue(tokenResponse); - keyService.hasUserKey.mockResolvedValue(true); - const adminAuthResponse = { - id: "1", - publicKey: "PRIVATE" as any, - key: "KEY" as any, - masterPasswordHash: "HASH" as any, - requestApproved: true, - }; - apiService.getAuthRequest.mockResolvedValue(adminAuthResponse as AuthRequestResponse); - - await ssoLoginStrategy.logIn(credentials); - - expect(authRequestService.setKeysAfterDecryptingSharedMasterKeyAndHash).toHaveBeenCalled(); - expect(deviceTrustService.decryptUserKeyWithDeviceKey).not.toHaveBeenCalled(); - }); - it("sets the user key from approved admin request if exists", async () => { apiService.postIdentityToken.mockResolvedValue(tokenResponse); keyService.hasUserKey.mockResolvedValue(true); @@ -475,9 +457,6 @@ describe("SsoLoginStrategy", () => { await ssoLoginStrategy.logIn(credentials); expect(authRequestService.clearAdminAuthRequest).toHaveBeenCalled(); - expect( - authRequestService.setKeysAfterDecryptingSharedMasterKeyAndHash, - ).not.toHaveBeenCalled(); expect(authRequestService.setUserKeyAfterDecryptingSharedUserKey).not.toHaveBeenCalled(); expect(deviceTrustService.trustDeviceIfRequired).not.toHaveBeenCalled(); }); diff --git a/libs/auth/src/common/login-strategies/sso-login.strategy.ts b/libs/auth/src/common/login-strategies/sso-login.strategy.ts index 6a57d11e29d..9c889d9d460 100644 --- a/libs/auth/src/common/login-strategies/sso-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/sso-login.strategy.ts @@ -239,23 +239,11 @@ export class SsoLoginStrategy extends LoginStrategy { } if (adminAuthReqResponse?.requestApproved) { - // if masterPasswordHash has a value, we will always receive authReqResponse.key - // as authRequestPublicKey(masterKey) + authRequestPublicKey(masterPasswordHash) - if (adminAuthReqResponse.masterPasswordHash) { - await this.authRequestService.setKeysAfterDecryptingSharedMasterKeyAndHash( - adminAuthReqResponse, - adminAuthReqStorable.privateKey, - userId, - ); - } else { - // if masterPasswordHash is null, we will always receive authReqResponse.key - // as authRequestPublicKey(userKey) - await this.authRequestService.setUserKeyAfterDecryptingSharedUserKey( - adminAuthReqResponse, - adminAuthReqStorable.privateKey, - userId, - ); - } + await this.authRequestService.setUserKeyAfterDecryptingSharedUserKey( + adminAuthReqResponse, + adminAuthReqStorable.privateKey, + userId, + ); if (await this.keyService.hasUserKey(userId)) { // Now that we have a decrypted user key in memory, we can check if we diff --git a/libs/auth/src/common/models/domain/login-credentials.ts b/libs/auth/src/common/models/domain/login-credentials.ts index 96ee88945eb..608b36e908b 100644 --- a/libs/auth/src/common/models/domain/login-credentials.ts +++ b/libs/auth/src/common/models/domain/login-credentials.ts @@ -7,7 +7,7 @@ import { AuthenticationType } from "@bitwarden/common/auth/enums/authentication- import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request"; import { WebAuthnLoginAssertionResponseRequest } from "@bitwarden/common/auth/services/webauthn-login/request/webauthn-login-assertion-response.request"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; -import { UserKey, MasterKey } from "@bitwarden/common/types/key"; +import { UserKey } from "@bitwarden/common/types/key"; export class PasswordLoginCredentials { readonly type = AuthenticationType.Password; @@ -54,8 +54,6 @@ export class AuthRequestLoginCredentials { public accessCode: string, public authRequestId: string, public decryptedUserKey: UserKey | null, - public decryptedMasterKey: MasterKey | null, - public decryptedMasterKeyHash: string | null, public twoFactor?: TokenTwoFactorRequest, ) {} @@ -66,8 +64,6 @@ export class AuthRequestLoginCredentials { json.accessCode, json.authRequestId, null, - null, - json.decryptedMasterKeyHash, json.twoFactor ? new TokenTwoFactorRequest( json.twoFactor.provider, @@ -78,7 +74,6 @@ export class AuthRequestLoginCredentials { ), { decryptedUserKey: SymmetricCryptoKey.fromJSON(json.decryptedUserKey) as UserKey, - decryptedMasterKey: SymmetricCryptoKey.fromJSON(json.decryptedMasterKey) as MasterKey, }, ); } diff --git a/libs/auth/src/common/services/auth-request/auth-request.service.spec.ts b/libs/auth/src/common/services/auth-request/auth-request.service.spec.ts index 8cb0cc279ae..a3f79f45ad5 100644 --- a/libs/auth/src/common/services/auth-request/auth-request.service.spec.ts +++ b/libs/auth/src/common/services/auth-request/auth-request.service.spec.ts @@ -13,7 +13,7 @@ import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.ser import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { StateProvider } from "@bitwarden/common/platform/state"; import { UserId } from "@bitwarden/common/types/guid"; -import { MasterKey, UserKey } from "@bitwarden/common/types/key"; +import { UserKey } from "@bitwarden/common/types/key"; import { newGuid } from "@bitwarden/guid"; import { KeyService } from "@bitwarden/key-management"; @@ -154,60 +154,6 @@ describe("AuthRequestService", () => { }); }); - describe("setKeysAfterDecryptingSharedMasterKeyAndHash", () => { - it("decrypts and sets master key and hash and user key when given valid auth request response and private key", async () => { - // Arrange - const mockAuthReqResponse = { - key: "authReqPublicKeyEncryptedMasterKey", - masterPasswordHash: "authReqPublicKeyEncryptedMasterKeyHash", - } as AuthRequestResponse; - - const mockDecryptedMasterKey = {} as MasterKey; - const mockDecryptedMasterKeyHash = "mockDecryptedMasterKeyHash"; - const mockDecryptedUserKey = {} as UserKey; - - jest.spyOn(sut, "decryptPubKeyEncryptedMasterKeyAndHash").mockResolvedValueOnce({ - masterKey: mockDecryptedMasterKey, - masterKeyHash: mockDecryptedMasterKeyHash, - }); - - masterPasswordService.masterKeySubject.next(undefined); - masterPasswordService.masterKeyHashSubject.next(undefined); - masterPasswordService.mock.decryptUserKeyWithMasterKey.mockResolvedValue( - mockDecryptedUserKey, - ); - keyService.setUserKey.mockResolvedValueOnce(undefined); - - // Act - await sut.setKeysAfterDecryptingSharedMasterKeyAndHash( - mockAuthReqResponse, - mockPrivateKey, - mockUserId, - ); - - // Assert - expect(sut.decryptPubKeyEncryptedMasterKeyAndHash).toBeCalledWith( - mockAuthReqResponse.key, - mockAuthReqResponse.masterPasswordHash, - mockPrivateKey, - ); - expect(masterPasswordService.mock.setMasterKey).toHaveBeenCalledWith( - mockDecryptedMasterKey, - mockUserId, - ); - expect(masterPasswordService.mock.setMasterKeyHash).toHaveBeenCalledWith( - mockDecryptedMasterKeyHash, - mockUserId, - ); - expect(masterPasswordService.mock.decryptUserKeyWithMasterKey).toHaveBeenCalledWith( - mockDecryptedMasterKey, - mockUserId, - undefined, - ); - expect(keyService.setUserKey).toHaveBeenCalledWith(mockDecryptedUserKey, mockUserId); - }); - }); - describe("decryptAuthReqPubKeyEncryptedUserKey", () => { it("returns a decrypted user key when given valid public key encrypted user key and an auth req private key", async () => { // Arrange diff --git a/libs/auth/src/common/services/auth-request/auth-request.service.ts b/libs/auth/src/common/services/auth-request/auth-request.service.ts index ba4b9eaf174..f1ff8416d11 100644 --- a/libs/auth/src/common/services/auth-request/auth-request.service.ts +++ b/libs/auth/src/common/services/auth-request/auth-request.service.ts @@ -16,14 +16,13 @@ import { ListResponse } from "@bitwarden/common/models/response/list.response"; import { AuthRequestPushNotification } from "@bitwarden/common/models/response/notification.response"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { AUTH_REQUEST_DISK_LOCAL, StateProvider, UserKeyDefinition, } from "@bitwarden/common/platform/state"; import { UserId } from "@bitwarden/common/types/guid"; -import { MasterKey, UserKey } from "@bitwarden/common/types/key"; +import { UserKey } from "@bitwarden/common/types/key"; import { KeyService } from "@bitwarden/key-management"; import { AuthRequestApiServiceAbstraction } from "../../abstractions/auth-request-api.service"; @@ -163,27 +162,6 @@ export class AuthRequestService implements AuthRequestServiceAbstraction { await this.keyService.setUserKey(userKey, userId); } - async setKeysAfterDecryptingSharedMasterKeyAndHash( - authReqResponse: AuthRequestResponse, - authReqPrivateKey: Uint8Array, - userId: UserId, - ) { - const { masterKey, masterKeyHash } = await this.decryptPubKeyEncryptedMasterKeyAndHash( - authReqResponse.key, - authReqResponse.masterPasswordHash, - authReqPrivateKey, - ); - - // Decrypt and set user key in state - const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(masterKey, userId); - - // Set masterKey + masterKeyHash in state after decryption (in case decryption fails) - await this.masterPasswordService.setMasterKey(masterKey, userId); - await this.masterPasswordService.setMasterKeyHash(masterKeyHash, userId); - - await this.keyService.setUserKey(userKey, userId); - } - // Decryption helpers async decryptPubKeyEncryptedUserKey( pubKeyEncryptedUserKey: string, @@ -197,30 +175,6 @@ export class AuthRequestService implements AuthRequestServiceAbstraction { return decryptedUserKey as UserKey; } - async decryptPubKeyEncryptedMasterKeyAndHash( - pubKeyEncryptedMasterKey: string, - pubKeyEncryptedMasterKeyHash: string, - privateKey: Uint8Array, - ): Promise<{ masterKey: MasterKey; masterKeyHash: string }> { - const decryptedMasterKeyArrayBuffer = await this.encryptService.rsaDecrypt( - new EncString(pubKeyEncryptedMasterKey), - privateKey, - ); - - const decryptedMasterKeyHashArrayBuffer = await this.encryptService.rsaDecrypt( - new EncString(pubKeyEncryptedMasterKeyHash), - privateKey, - ); - - const masterKey = new SymmetricCryptoKey(decryptedMasterKeyArrayBuffer) as MasterKey; - const masterKeyHash = Utils.fromBufferToUtf8(decryptedMasterKeyHashArrayBuffer); - - return { - masterKey, - masterKeyHash, - }; - } - sendAuthRequestPushNotification(notification: AuthRequestPushNotification): void { if (notification.id != null) { this.authRequestPushNotificationSubject.next(notification.id); diff --git a/libs/auth/src/common/services/login-strategies/login-strategy.state.spec.ts b/libs/auth/src/common/services/login-strategies/login-strategy.state.spec.ts index 32c5fdcc4d5..195ae0dd721 100644 --- a/libs/auth/src/common/services/login-strategies/login-strategy.state.spec.ts +++ b/libs/auth/src/common/services/login-strategies/login-strategy.state.spec.ts @@ -93,8 +93,6 @@ describe("LOGIN_STRATEGY_CACHE_KEY", () => { "ACCESS_CODE", "AUTH_REQUEST_ID", new SymmetricCryptoKey(new Uint8Array(64)) as UserKey, - new SymmetricCryptoKey(new Uint8Array(64)) as MasterKey, - "MASTER_KEY_HASH", ); const result = sut.deserializer(JSON.parse(JSON.stringify(actual))); diff --git a/libs/common/src/auth/models/response/auth-request.response.ts b/libs/common/src/auth/models/response/auth-request.response.ts index 94c65000919..8d02e161e68 100644 --- a/libs/common/src/auth/models/response/auth-request.response.ts +++ b/libs/common/src/auth/models/response/auth-request.response.ts @@ -11,8 +11,7 @@ export class AuthRequestResponse extends BaseResponse { requestDeviceIdentifier: string; requestIpAddress: string; requestCountryName: string; - key: string; // could be either an encrypted MasterKey or an encrypted UserKey - masterPasswordHash: string; // if hash is present, the `key` above is an encrypted MasterKey (else `key` is an encrypted UserKey) + key: string; // Auth-request public-key encrypted user-key. Note: No sender authenticity provided! creationDate: string; requestApproved?: boolean; responseDate?: string; @@ -30,7 +29,6 @@ export class AuthRequestResponse extends BaseResponse { this.requestIpAddress = this.getResponseProperty("RequestIpAddress"); this.requestCountryName = this.getResponseProperty("RequestCountryName"); this.key = this.getResponseProperty("Key"); - this.masterPasswordHash = this.getResponseProperty("MasterPasswordHash"); this.creationDate = this.getResponseProperty("CreationDate"); this.requestApproved = this.getResponseProperty("RequestApproved"); this.responseDate = this.getResponseProperty("ResponseDate"); diff --git a/libs/common/src/key-management/crypto/abstractions/encrypt.service.ts b/libs/common/src/key-management/crypto/abstractions/encrypt.service.ts index 03cfd173a4d..ed1458b704e 100644 --- a/libs/common/src/key-management/crypto/abstractions/encrypt.service.ts +++ b/libs/common/src/key-management/crypto/abstractions/encrypt.service.ts @@ -161,13 +161,6 @@ export abstract class EncryptService { decapsulationKey: Uint8Array, ): Promise; - /** - * @deprecated Use @see {@link decapsulateKeyUnsigned} instead - * @param data - The ciphertext to decrypt - * @param privateKey - The privateKey to decrypt with - */ - abstract rsaDecrypt(data: EncString, privateKey: Uint8Array): Promise; - /** * Generates a base64-encoded hash of the given value * @param value The value to hash diff --git a/libs/common/src/key-management/crypto/services/encrypt.service.implementation.ts b/libs/common/src/key-management/crypto/services/encrypt.service.implementation.ts index b14211b5b72..5fa5fa5eea0 100644 --- a/libs/common/src/key-management/crypto/services/encrypt.service.implementation.ts +++ b/libs/common/src/key-management/crypto/services/encrypt.service.implementation.ts @@ -219,24 +219,4 @@ export class EncryptServiceImplementation implements EncryptService { ); return new SymmetricCryptoKey(keyBytes); } - - async rsaDecrypt(data: EncString, privateKey: Uint8Array): Promise { - if (data == null) { - throw new Error("[Encrypt service] rsaDecrypt: No data provided for decryption."); - } - - switch (data.encryptionType) { - case EncryptionType.Rsa2048_OaepSha1_B64: - case EncryptionType.Rsa2048_OaepSha1_HmacSha256_B64: - break; - default: - throw new Error("Invalid encryption type."); - } - - if (privateKey == null) { - throw new Error("[Encrypt service] rsaDecrypt: No private key provided for decryption."); - } - - return this.cryptoFunctionService.rsaDecrypt(data.dataBytes, privateKey, "sha1"); - } } From 84845024fdc264c4843cc797ba6d62053205b740 Mon Sep 17 00:00:00 2001 From: Alex Dragovich <46065570+itsadrago@users.noreply.github.com> Date: Fri, 20 Feb 2026 10:29:40 -0800 Subject: [PATCH 55/89] [PM-32502] fixed icon / copy value spacing in button on send access page (#19092) * [PM-32502] fixed icon / copy value spacing in button on send access page * [PM-32502] using more approriate button configuration on send access --- .../send/send-access/send-access-text.component.html | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/apps/web/src/app/tools/send/send-access/send-access-text.component.html b/apps/web/src/app/tools/send/send-access/send-access-text.component.html index c7fa148169d..746dd5f0567 100644 --- a/apps/web/src/app/tools/send/send-access/send-access-text.component.html +++ b/apps/web/src/app/tools/send/send-access/send-access-text.component.html @@ -10,7 +10,14 @@ }

-
From 72be42ed681398561a57171126abf900760760a8 Mon Sep 17 00:00:00 2001 From: Brad <44413459+lastbestdev@users.noreply.github.com> Date: Fri, 20 Feb 2026 11:11:17 -0800 Subject: [PATCH 56/89] [PM-32127] Access Intelligence: persist selected applications through filter updates (#18990) This PR includes changes to the Access Intelligence table view, which keep Applications selected in the table as the user makes changes to filters (search bar, critical applications filter). This required updating logic to ensure only visible rows in the table are considered for updates to critical status with the "Mark # as critical" button, while still maintaining the full list of selected applications in the component's selectedUrls. The Applications table component is also refactored to use Angular output for checkbox state, emitting events on checkbox changes for individual table rows and "select all". The parent component handles these events by updating the set of selected Applications (selectedUrls) accordingly. Test cases are updated/added to cover the updated checkbox functionality. --- .../applications.component.html | 9 +- .../applications.component.spec.ts | 202 +++++++++++++++++- .../applications.component.ts | 85 +++++--- ...pp-table-row-scrollable-m11.component.html | 2 +- ...table-row-scrollable-m11.component.spec.ts | 74 ++++++- .../app-table-row-scrollable-m11.component.ts | 24 +-- 6 files changed, 339 insertions(+), 57 deletions(-) diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications/applications.component.html b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications/applications.component.html index 743f8ff1b68..ec73c4f47e6 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications/applications.component.html +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications/applications.component.html @@ -22,7 +22,7 @@
- @if (selectedUrls().size > 0) { + @if (visibleSelectedApps().size > 0) { @if (allSelectedAppsAreCritical()) { } @else { } } @@ -79,8 +79,9 @@ [dataSource]="dataSource" [selectedUrls]="selectedUrls()" [openApplication]="drawerDetails.invokerId || ''" - [checkboxChange]="onCheckboxChange" [showAppAtRiskMembers]="showAppAtRiskMembers" + (checkboxChange)="onCheckboxChange($event)" + (selectAllChange)="onSelectAllChange($event)" class="tw-mb-10" > diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications/applications.component.spec.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications/applications.component.spec.ts index b4cbbc5c436..b79f5160bf7 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications/applications.component.spec.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications/applications.component.spec.ts @@ -1,8 +1,9 @@ +import { Signal, WritableSignal } from "@angular/core"; import { ComponentFixture, TestBed } from "@angular/core/testing"; import { ReactiveFormsModule } from "@angular/forms"; -import { ActivatedRoute } from "@angular/router"; +import { ActivatedRoute, convertToParamMap } from "@angular/router"; import { mock, MockProxy } from "jest-mock-extended"; -import { BehaviorSubject } from "rxjs"; +import { BehaviorSubject, of } from "rxjs"; import { DrawerDetails, @@ -11,6 +12,7 @@ import { ReportStatus, RiskInsightsDataService, } from "@bitwarden/bit-common/dirt/reports/risk-insights"; +import { createNewSummaryData } from "@bitwarden/bit-common/dirt/reports/risk-insights/helpers"; import { RiskInsightsEnrichedData } from "@bitwarden/bit-common/dirt/reports/risk-insights/models/report-data-service.types"; import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -23,9 +25,18 @@ import { AccessIntelligenceSecurityTasksService } from "../shared/security-tasks import { ApplicationsComponent } from "./applications.component"; +// Mock ResizeObserver +global.ResizeObserver = class ResizeObserver { + observe() {} + unobserve() {} + disconnect() {} +}; + // Helper type to access protected members in tests type ComponentWithProtectedMembers = ApplicationsComponent & { dataSource: TableDataSource; + selectedUrls: WritableSignal>; + filteredTableData: Signal; }; describe("ApplicationsComponent", () => { @@ -83,7 +94,10 @@ describe("ApplicationsComponent", () => { { provide: RiskInsightsDataService, useValue: mockDataService }, { provide: ActivatedRoute, - useValue: { snapshot: { paramMap: { get: (): string | null => null } } }, + useValue: { + paramMap: of(convertToParamMap({})), + snapshot: { paramMap: convertToParamMap({}) }, + }, }, { provide: AccessIntelligenceSecurityTasksService, useValue: mockSecurityTasksService }, ], @@ -91,6 +105,7 @@ describe("ApplicationsComponent", () => { fixture = TestBed.createComponent(ApplicationsComponent); component = fixture.componentInstance; + fixture.detectChanges(); }); afterEach(() => { @@ -247,4 +262,185 @@ describe("ApplicationsComponent", () => { expect(capturedBlobData).not.toContain("Slack"); }); }); + + describe("checkbox selection", () => { + const mockApplicationData: ApplicationTableDataSource[] = [ + { + applicationName: "GitHub", + passwordCount: 10, + atRiskPasswordCount: 3, + memberCount: 5, + atRiskMemberCount: 2, + isMarkedAsCritical: true, + atRiskCipherIds: ["cipher1" as CipherId], + memberDetails: [] as MemberDetails[], + atRiskMemberDetails: [] as MemberDetails[], + cipherIds: ["cipher1" as CipherId], + iconCipher: undefined, + }, + { + applicationName: "Slack", + passwordCount: 8, + atRiskPasswordCount: 1, + memberCount: 4, + atRiskMemberCount: 1, + isMarkedAsCritical: false, + atRiskCipherIds: ["cipher2" as CipherId], + memberDetails: [] as MemberDetails[], + atRiskMemberDetails: [] as MemberDetails[], + cipherIds: ["cipher2" as CipherId], + iconCipher: undefined, + }, + { + applicationName: "Jira", + passwordCount: 12, + atRiskPasswordCount: 4, + memberCount: 6, + atRiskMemberCount: 3, + isMarkedAsCritical: false, + atRiskCipherIds: ["cipher3" as CipherId], + memberDetails: [] as MemberDetails[], + atRiskMemberDetails: [] as MemberDetails[], + cipherIds: ["cipher3" as CipherId], + iconCipher: undefined, + }, + ]; + + beforeEach(() => { + // Emit mock data through the data service observable to populate the table + enrichedReportData$.next({ + reportData: mockApplicationData, + summaryData: createNewSummaryData(), + applicationData: [], + creationDate: new Date(), + }); + }); + + describe("onCheckboxChange", () => { + it("should add application to selectedUrls when checked is true", () => { + // act + component.onCheckboxChange({ applicationName: "GitHub", checked: true }); + + // assert + const selectedUrls = (component as ComponentWithProtectedMembers).selectedUrls(); + expect(selectedUrls.has("GitHub")).toBe(true); + expect(selectedUrls.size).toBe(1); + }); + + it("should remove application from selectedUrls when checked is false", () => { + // arrange + (component as ComponentWithProtectedMembers).selectedUrls.set(new Set(["GitHub", "Slack"])); + + // act + component.onCheckboxChange({ applicationName: "GitHub", checked: false }); + + // assert + const selectedUrls = (component as ComponentWithProtectedMembers).selectedUrls(); + expect(selectedUrls.has("GitHub")).toBe(false); + expect(selectedUrls.has("Slack")).toBe(true); + expect(selectedUrls.size).toBe(1); + }); + + it("should handle multiple applications being selected", () => { + // act + component.onCheckboxChange({ applicationName: "GitHub", checked: true }); + component.onCheckboxChange({ applicationName: "Slack", checked: true }); + component.onCheckboxChange({ applicationName: "Jira", checked: true }); + + // assert + const selectedUrls = (component as ComponentWithProtectedMembers).selectedUrls(); + expect(selectedUrls.has("GitHub")).toBe(true); + expect(selectedUrls.has("Slack")).toBe(true); + expect(selectedUrls.has("Jira")).toBe(true); + expect(selectedUrls.size).toBe(3); + }); + }); + + describe("onSelectAllChange", () => { + it("should add all visible applications to selectedUrls when checked is true", () => { + // act + component.onSelectAllChange(true); + + // assert + const selectedUrls = (component as ComponentWithProtectedMembers).selectedUrls(); + expect(selectedUrls.has("GitHub")).toBe(true); + expect(selectedUrls.has("Slack")).toBe(true); + expect(selectedUrls.has("Jira")).toBe(true); + expect(selectedUrls.size).toBe(3); + }); + + it("should remove all applications from selectedUrls when checked is false", () => { + // arrange + (component as ComponentWithProtectedMembers).selectedUrls.set(new Set(["GitHub", "Slack"])); + + // act + component.onSelectAllChange(false); + + // assert + const selectedUrls = (component as ComponentWithProtectedMembers).selectedUrls(); + expect(selectedUrls.size).toBe(0); + }); + + it("should only add visible filtered applications when filter is applied", () => { + // arrange - apply filter to only show critical apps + (component as ComponentWithProtectedMembers).dataSource.filter = ( + app: ApplicationTableDataSource, + ) => app.isMarkedAsCritical; + fixture.detectChanges(); + + // act + component.onSelectAllChange(true); + + // assert - only GitHub is critical + const selectedUrls = (component as ComponentWithProtectedMembers).selectedUrls(); + expect(selectedUrls.has("GitHub")).toBe(true); + expect(selectedUrls.has("Slack")).toBe(false); + expect(selectedUrls.has("Jira")).toBe(false); + expect(selectedUrls.size).toBe(1); + }); + + it("should only remove visible filtered applications when unchecking with filter applied", () => { + // arrange - select all apps first, then apply filter to only show non-critical apps + (component as ComponentWithProtectedMembers).selectedUrls.set( + new Set(["GitHub", "Slack", "Jira"]), + ); + + (component as ComponentWithProtectedMembers).dataSource.filter = ( + app: ApplicationTableDataSource, + ) => !app.isMarkedAsCritical; + fixture.detectChanges(); + + // act - uncheck with filter applied + component.onSelectAllChange(false); + + // assert - GitHub (critical) should still be selected + const selectedUrls = (component as ComponentWithProtectedMembers).selectedUrls(); + expect(selectedUrls.has("GitHub")).toBe(true); + expect(selectedUrls.has("Slack")).toBe(false); + expect(selectedUrls.has("Jira")).toBe(false); + expect(selectedUrls.size).toBe(1); + }); + + it("should preserve existing selections when checking select all with filter", () => { + // arrange - select a non-visible app + (component as ComponentWithProtectedMembers).selectedUrls.set(new Set(["GitHub"])); + + // apply filter to hide GitHub + (component as ComponentWithProtectedMembers).dataSource.filter = ( + app: ApplicationTableDataSource, + ) => !app.isMarkedAsCritical; + fixture.detectChanges(); + + // act - select all visible (non-critical apps) + component.onSelectAllChange(true); + + // assert - GitHub should still be selected + visible apps + const selectedUrls = (component as ComponentWithProtectedMembers).selectedUrls(); + expect(selectedUrls.has("GitHub")).toBe(true); + expect(selectedUrls.has("Slack")).toBe(true); + expect(selectedUrls.has("Jira")).toBe(true); + expect(selectedUrls.size).toBe(3); + }); + }); + }); }); diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications/applications.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications/applications.component.ts index 962628584d3..659e099641c 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications/applications.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications/applications.component.ts @@ -89,6 +89,9 @@ export class ApplicationsComponent implements OnInit { // Standard properties protected readonly dataSource = new TableDataSource(); protected readonly searchControl = new FormControl("", { nonNullable: true }); + protected readonly filteredTableData = toSignal(this.dataSource.connect(), { + initialValue: [], + }); // Template driven properties protected readonly selectedUrls = signal(new Set()); @@ -118,13 +121,35 @@ export class ApplicationsComponent implements OnInit { }, ]); + // Computed property that returns only selected applications that are currently visible in filtered data + readonly visibleSelectedApps = computed(() => { + const filteredData = this.filteredTableData(); + const selected = this.selectedUrls(); + + if (!filteredData || selected.size === 0) { + return new Set(); + } + + const visibleSelected = new Set(); + filteredData.forEach((row) => { + if (selected.has(row.applicationName)) { + visibleSelected.add(row.applicationName); + } + }); + + return visibleSelected; + }); + readonly allSelectedAppsAreCritical = computed(() => { - if (!this.dataSource.filteredData || this.selectedUrls().size == 0) { + const visibleSelected = this.visibleSelectedApps(); + const filteredData = this.filteredTableData(); + + if (!filteredData || visibleSelected.size === 0) { return false; } - return this.dataSource.filteredData - .filter((row) => this.selectedUrls().has(row.applicationName)) + return filteredData + .filter((row) => visibleSelected.has(row.applicationName)) .every((row) => row.isMarkedAsCritical); }); @@ -202,15 +227,6 @@ export class ApplicationsComponent implements OnInit { this.dataSource.filter = (app) => filterFunction(app) && app.applicationName.toLowerCase().includes(searchText.toLowerCase()); - - // filter selectedUrls down to only applications showing with active filters - const filteredUrls = new Set(); - this.dataSource.filteredData?.forEach((row) => { - if (this.selectedUrls().has(row.applicationName)) { - filteredUrls.add(row.applicationName); - } - }); - this.selectedUrls.set(filteredUrls); }); } @@ -218,12 +234,13 @@ export class ApplicationsComponent implements OnInit { this.selectedFilter.set(value); } - markAppsAsCritical = async () => { + async markAppsAsCritical() { this.updatingCriticalApps.set(true); - const count = this.selectedUrls().size; + const visibleSelected = this.visibleSelectedApps(); + const count = visibleSelected.size; this.dataService - .saveCriticalApplications(Array.from(this.selectedUrls())) + .saveCriticalApplications(Array.from(visibleSelected)) .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe({ next: (response) => { @@ -246,11 +263,11 @@ export class ApplicationsComponent implements OnInit { }); }, }); - }; + } - unmarkAppsAsCritical = async () => { + async unmarkAppsAsCritical() { this.updatingCriticalApps.set(true); - const appsToUnmark = this.selectedUrls(); + const appsToUnmark = this.visibleSelectedApps(); this.dataService .removeCriticalApplications(appsToUnmark) @@ -278,7 +295,7 @@ export class ApplicationsComponent implements OnInit { }); }, }); - }; + } async requestPasswordChange() { const orgId = this.organizationId(); @@ -310,24 +327,38 @@ export class ApplicationsComponent implements OnInit { } } - showAppAtRiskMembers = async (applicationName: string) => { + async showAppAtRiskMembers(applicationName: string) { await this.dataService.setDrawerForAppAtRiskMembers(applicationName); - }; + } - onCheckboxChange = (applicationName: string, event: Event) => { - const isChecked = (event.target as HTMLInputElement).checked; + onCheckboxChange({ applicationName, checked }: { applicationName: string; checked: boolean }) { this.selectedUrls.update((selectedUrls) => { const nextSelected = new Set(selectedUrls); - if (isChecked) { + if (checked) { nextSelected.add(applicationName); } else { nextSelected.delete(applicationName); } return nextSelected; }); - }; + } - downloadApplicationsCSV = () => { + onSelectAllChange(checked: boolean) { + const filteredData = this.filteredTableData(); + if (!filteredData) { + return; + } + + this.selectedUrls.update((selectedUrls) => { + const nextSelected = new Set(selectedUrls); + filteredData.forEach((row) => + checked ? nextSelected.add(row.applicationName) : nextSelected.delete(row.applicationName), + ); + return nextSelected; + }); + } + + downloadApplicationsCSV() { try { const data = this.dataSource.filteredData; if (!data || data.length === 0) { @@ -360,5 +391,5 @@ export class ApplicationsComponent implements OnInit { } catch (error) { this.logService.error("Failed to download applications CSV", error); } - }; + } } diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/app-table-row-scrollable-m11.component.html b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/app-table-row-scrollable-m11.component.html index 05dec048328..ddbc977fc13 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/app-table-row-scrollable-m11.component.html +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/app-table-row-scrollable-m11.component.html @@ -33,7 +33,7 @@ bitCheckbox type="checkbox" [checked]="selectedUrls().has(row.applicationName)" - (change)="checkboxChange()(row.applicationName, $event)" + (change)="checkboxChanged($event.target, row.applicationName)" /> { selectAllCheckboxEl = fixture.debugElement.query(By.css('[data-testid="selectAll"]')); }); - it("should check all rows in table when checked", () => { + it("should emit selectAllChange event with true when checked", () => { // arrange const selectedUrls = new Set(); const dataSource = new TableDataSource(); @@ -121,18 +121,19 @@ describe("AppTableRowScrollableM11Component", () => { fixture.componentRef.setInput("dataSource", dataSource); fixture.detectChanges(); + const selectAllChangeSpy = jest.fn(); + fixture.componentInstance.selectAllChange.subscribe(selectAllChangeSpy); + // act selectAllCheckboxEl.nativeElement.click(); fixture.detectChanges(); // assert - expect(selectedUrls.has("google.com")).toBe(true); - expect(selectedUrls.has("facebook.com")).toBe(true); - expect(selectedUrls.has("twitter.com")).toBe(true); - expect(selectedUrls.size).toBe(3); + expect(selectAllChangeSpy).toHaveBeenCalledWith(true); + expect(selectAllChangeSpy).toHaveBeenCalledTimes(1); }); - it("should uncheck all rows in table when unchecked", () => { + it("should emit selectAllChange event with false when unchecked", () => { // arrange const selectedUrls = new Set(["google.com", "facebook.com", "twitter.com"]); const dataSource = new TableDataSource(); @@ -142,12 +143,16 @@ describe("AppTableRowScrollableM11Component", () => { fixture.componentRef.setInput("dataSource", dataSource); fixture.detectChanges(); + const selectAllChangeSpy = jest.fn(); + fixture.componentInstance.selectAllChange.subscribe(selectAllChangeSpy); + // act selectAllCheckboxEl.nativeElement.click(); fixture.detectChanges(); // assert - expect(selectedUrls.size).toBe(0); + expect(selectAllChangeSpy).toHaveBeenCalledWith(false); + expect(selectAllChangeSpy).toHaveBeenCalledTimes(1); }); it("should become checked when all rows in table are checked", () => { @@ -178,4 +183,59 @@ describe("AppTableRowScrollableM11Component", () => { expect(selectAllCheckboxEl.nativeElement.checked).toBe(false); }); }); + + describe("individual row checkbox", () => { + it("should emit checkboxChange event with correct parameters when checkboxChanged is called", () => { + // arrange + const checkboxChangeSpy = jest.fn(); + fixture.componentInstance.checkboxChange.subscribe(checkboxChangeSpy); + + const mockTarget = { checked: true } as HTMLInputElement; + + // act + fixture.componentInstance.checkboxChanged(mockTarget, "google.com"); + + // assert + expect(checkboxChangeSpy).toHaveBeenCalledWith({ + applicationName: "google.com", + checked: true, + }); + expect(checkboxChangeSpy).toHaveBeenCalledTimes(1); + }); + + it("should emit checkboxChange with checked=false when checkbox is unchecked", () => { + // arrange + const checkboxChangeSpy = jest.fn(); + fixture.componentInstance.checkboxChange.subscribe(checkboxChangeSpy); + + const mockTarget = { checked: false } as HTMLInputElement; + + // act + fixture.componentInstance.checkboxChanged(mockTarget, "google.com"); + + // assert + expect(checkboxChangeSpy).toHaveBeenCalledWith({ + applicationName: "google.com", + checked: false, + }); + expect(checkboxChangeSpy).toHaveBeenCalledTimes(1); + }); + + it("should emit checkboxChange with correct applicationName for different applications", () => { + // arrange + const checkboxChangeSpy = jest.fn(); + fixture.componentInstance.checkboxChange.subscribe(checkboxChangeSpy); + + const mockTarget = { checked: true } as HTMLInputElement; + + // act + fixture.componentInstance.checkboxChanged(mockTarget, "facebook.com"); + + // assert + expect(checkboxChangeSpy).toHaveBeenCalledWith({ + applicationName: "facebook.com", + checked: true, + }); + }); + }); }); diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/app-table-row-scrollable-m11.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/app-table-row-scrollable-m11.component.ts index a23d1855ba5..65cfb8d092e 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/app-table-row-scrollable-m11.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/app-table-row-scrollable-m11.component.ts @@ -1,5 +1,5 @@ import { CommonModule } from "@angular/common"; -import { ChangeDetectionStrategy, Component, input } from "@angular/core"; +import { ChangeDetectionStrategy, Component, input, output } from "@angular/core"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { MenuModule, TableDataSource, TableModule, TooltipDirective } from "@bitwarden/components"; @@ -30,7 +30,8 @@ export class AppTableRowScrollableM11Component { readonly selectedUrls = input>(); readonly openApplication = input(""); readonly showAppAtRiskMembers = input<(applicationName: string) => void>(); - readonly checkboxChange = input<(applicationName: string, $event: Event) => void>(); + readonly checkboxChange = output<{ applicationName: string; checked: boolean }>(); + readonly selectAllChange = output(); allAppsSelected(): boolean { const tableData = this.dataSource()?.filteredData; @@ -43,20 +44,13 @@ export class AppTableRowScrollableM11Component { return tableData.length > 0 && tableData.every((row) => selectedUrls.has(row.applicationName)); } + checkboxChanged(target: HTMLInputElement, applicationName: string) { + const checked = target.checked; + this.checkboxChange.emit({ applicationName, checked }); + } + selectAllChanged(target: HTMLInputElement) { const checked = target.checked; - - const tableData = this.dataSource()?.filteredData; - const selectedUrls = this.selectedUrls(); - - if (!tableData || !selectedUrls) { - return false; - } - - if (checked) { - tableData.forEach((row) => selectedUrls.add(row.applicationName)); - } else { - selectedUrls.clear(); - } + this.selectAllChange.emit(checked); } } From 531a9df6b01565f931fd3f89093e8d351c170fb0 Mon Sep 17 00:00:00 2001 From: Github Actions Date: Fri, 20 Feb 2026 19:25:42 +0000 Subject: [PATCH 57/89] Bumped Desktop client to 2026.2.1 --- apps/desktop/package.json | 2 +- apps/desktop/src/package-lock.json | 4 ++-- apps/desktop/src/package.json | 2 +- package-lock.json | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/desktop/package.json b/apps/desktop/package.json index cd2147d21e4..5718c752a7c 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": "2026.2.0", + "version": "2026.2.1", "keywords": [ "bitwarden", "password", diff --git a/apps/desktop/src/package-lock.json b/apps/desktop/src/package-lock.json index 0aa188eba2f..01c429ab3d0 100644 --- a/apps/desktop/src/package-lock.json +++ b/apps/desktop/src/package-lock.json @@ -1,12 +1,12 @@ { "name": "@bitwarden/desktop", - "version": "2026.2.0", + "version": "2026.2.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@bitwarden/desktop", - "version": "2026.2.0", + "version": "2026.2.1", "license": "GPL-3.0", "dependencies": { "@bitwarden/desktop-napi": "file:../desktop_native/napi" diff --git a/apps/desktop/src/package.json b/apps/desktop/src/package.json index 0076981ab60..fac797b5344 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": "2026.2.0", + "version": "2026.2.1", "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 f5ac6ccbc0c..79541971b12 100644 --- a/package-lock.json +++ b/package-lock.json @@ -232,7 +232,7 @@ }, "apps/desktop": { "name": "@bitwarden/desktop", - "version": "2026.2.0", + "version": "2026.2.1", "hasInstallScript": true, "license": "GPL-3.0" }, From cae1ae649162c71a87efe388cce24d2419a730f9 Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Fri, 20 Feb 2026 13:45:30 -0600 Subject: [PATCH 58/89] Revert "Split NAPI modules [PM-31598] (#18722)" (#19112) This reverts commit fd90efabe4e31b169031902f29baf906785b2772. --- .github/CODEOWNERS | 3 - .../desktop_native/napi/src/autofill.rs | 332 ----- .../desktop_native/napi/src/autostart.rs | 9 - .../desktop_native/napi/src/autotype.rs | 20 - .../desktop_native/napi/src/biometrics.rs | 100 -- .../desktop_native/napi/src/biometrics_v2.rs | 116 -- .../napi/src/chromium_importer.rs | 116 -- .../desktop_native/napi/src/clipboards.rs | 15 - apps/desktop/desktop_native/napi/src/ipc.rs | 106 -- apps/desktop/desktop_native/napi/src/lib.rs | 1260 ++++++++++++++++- .../desktop_native/napi/src/logging.rs | 131 -- .../napi/src/passkey_authenticator.rs | 9 - .../desktop_native/napi/src/passwords.rs | 46 - .../desktop_native/napi/src/powermonitors.rs | 26 - .../napi/src/processisolations.rs | 23 - .../desktop_native/napi/src/sshagent.rs | 163 --- .../napi/src/windows_registry.rs | 16 - 17 files changed, 1241 insertions(+), 1250 deletions(-) delete mode 100644 apps/desktop/desktop_native/napi/src/autofill.rs delete mode 100644 apps/desktop/desktop_native/napi/src/autostart.rs delete mode 100644 apps/desktop/desktop_native/napi/src/autotype.rs delete mode 100644 apps/desktop/desktop_native/napi/src/biometrics.rs delete mode 100644 apps/desktop/desktop_native/napi/src/biometrics_v2.rs delete mode 100644 apps/desktop/desktop_native/napi/src/chromium_importer.rs delete mode 100644 apps/desktop/desktop_native/napi/src/clipboards.rs delete mode 100644 apps/desktop/desktop_native/napi/src/ipc.rs delete mode 100644 apps/desktop/desktop_native/napi/src/logging.rs delete mode 100644 apps/desktop/desktop_native/napi/src/passkey_authenticator.rs delete mode 100644 apps/desktop/desktop_native/napi/src/passwords.rs delete mode 100644 apps/desktop/desktop_native/napi/src/powermonitors.rs delete mode 100644 apps/desktop/desktop_native/napi/src/processisolations.rs delete mode 100644 apps/desktop/desktop_native/napi/src/sshagent.rs delete mode 100644 apps/desktop/desktop_native/napi/src/windows_registry.rs diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index c2e04d94f95..8f416e09511 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -163,9 +163,6 @@ apps/desktop/desktop_native/windows_plugin_authenticator @bitwarden/team-autofil apps/desktop/desktop_native/autotype @bitwarden/team-autofill-desktop-dev apps/desktop/desktop_native/core/src/ssh_agent @bitwarden/team-autofill-desktop-dev @bitwarden/wg-ssh-keys apps/desktop/desktop_native/ssh_agent @bitwarden/team-autofill-desktop-dev @bitwarden/wg-ssh-keys -apps/desktop/desktop_native/napi/src/autofill.rs @bitwarden/team-autofill-desktop-dev -apps/desktop/desktop_native/napi/src/autotype.rs @bitwarden/team-autofill-desktop-dev -apps/desktop/desktop_native/napi/src/sshagent.rs @bitwarden/team-autofill-desktop-dev # DuckDuckGo integration apps/desktop/native-messaging-test-runner @bitwarden/team-autofill-desktop-dev apps/desktop/src/services/duckduckgo-message-handler.service.ts @bitwarden/team-autofill-desktop-dev diff --git a/apps/desktop/desktop_native/napi/src/autofill.rs b/apps/desktop/desktop_native/napi/src/autofill.rs deleted file mode 100644 index 7717b22ccef..00000000000 --- a/apps/desktop/desktop_native/napi/src/autofill.rs +++ /dev/null @@ -1,332 +0,0 @@ -#[napi] -pub mod autofill { - use desktop_core::ipc::server::{Message, MessageType}; - use napi::{ - bindgen_prelude::FnArgs, - threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode}, - }; - use serde::{de::DeserializeOwned, Deserialize, Serialize}; - use tracing::error; - - #[napi] - pub async fn run_command(value: String) -> napi::Result { - desktop_core::autofill::run_command(value) - .await - .map_err(|e| napi::Error::from_reason(e.to_string())) - } - - #[derive(Debug, serde::Serialize, serde:: Deserialize)] - pub enum BitwardenError { - Internal(String), - } - - #[napi(string_enum)] - #[derive(Debug, Serialize, Deserialize)] - #[serde(rename_all = "camelCase")] - pub enum UserVerification { - #[napi(value = "preferred")] - Preferred, - #[napi(value = "required")] - Required, - #[napi(value = "discouraged")] - Discouraged, - } - - #[derive(Serialize, Deserialize)] - #[serde(bound = "T: Serialize + DeserializeOwned")] - pub struct PasskeyMessage { - pub sequence_number: u32, - pub value: Result, - } - - #[napi(object)] - #[derive(Debug, Serialize, Deserialize)] - #[serde(rename_all = "camelCase")] - pub struct Position { - pub x: i32, - pub y: i32, - } - - #[napi(object)] - #[derive(Debug, Serialize, Deserialize)] - #[serde(rename_all = "camelCase")] - pub struct PasskeyRegistrationRequest { - pub rp_id: String, - pub user_name: String, - pub user_handle: Vec, - pub client_data_hash: Vec, - pub user_verification: UserVerification, - pub supported_algorithms: Vec, - pub window_xy: Position, - pub excluded_credentials: Vec>, - } - - #[napi(object)] - #[derive(Serialize, Deserialize)] - #[serde(rename_all = "camelCase")] - pub struct PasskeyRegistrationResponse { - pub rp_id: String, - pub client_data_hash: Vec, - pub credential_id: Vec, - pub attestation_object: Vec, - } - - #[napi(object)] - #[derive(Debug, Serialize, Deserialize)] - #[serde(rename_all = "camelCase")] - pub struct PasskeyAssertionRequest { - pub rp_id: String, - pub client_data_hash: Vec, - pub user_verification: UserVerification, - pub allowed_credentials: Vec>, - pub window_xy: Position, - //extension_input: Vec, TODO: Implement support for extensions - } - - #[napi(object)] - #[derive(Debug, Serialize, Deserialize)] - #[serde(rename_all = "camelCase")] - pub struct PasskeyAssertionWithoutUserInterfaceRequest { - pub rp_id: String, - pub credential_id: Vec, - pub user_name: String, - pub user_handle: Vec, - pub record_identifier: Option, - pub client_data_hash: Vec, - pub user_verification: UserVerification, - pub window_xy: Position, - } - - #[napi(object)] - #[derive(Debug, Serialize, Deserialize)] - #[serde(rename_all = "camelCase")] - pub struct NativeStatus { - pub key: String, - pub value: String, - } - - #[napi(object)] - #[derive(Serialize, Deserialize)] - #[serde(rename_all = "camelCase")] - pub struct PasskeyAssertionResponse { - pub rp_id: String, - pub user_handle: Vec, - pub signature: Vec, - pub client_data_hash: Vec, - pub authenticator_data: Vec, - pub credential_id: Vec, - } - - #[napi] - pub struct AutofillIpcServer { - server: desktop_core::ipc::server::Server, - } - - // FIXME: Remove unwraps! They panic and terminate the whole application. - #[allow(clippy::unwrap_used)] - #[napi] - impl AutofillIpcServer { - /// Create and start the IPC server without blocking. - /// - /// @param name The endpoint name to listen on. This name uniquely identifies the IPC - /// connection and must be the same for both the server and client. @param callback - /// This function will be called whenever a message is received from a client. - #[allow(clippy::unused_async)] // FIXME: Remove unused async! - #[napi(factory)] - pub async fn listen( - name: String, - // Ideally we'd have a single callback that has an enum containing the request values, - // but NAPI doesn't support that just yet - #[napi( - ts_arg_type = "(error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyRegistrationRequest) => void" - )] - registration_callback: ThreadsafeFunction< - FnArgs<(u32, u32, PasskeyRegistrationRequest)>, - >, - #[napi( - ts_arg_type = "(error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyAssertionRequest) => void" - )] - assertion_callback: ThreadsafeFunction< - FnArgs<(u32, u32, PasskeyAssertionRequest)>, - >, - #[napi( - ts_arg_type = "(error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyAssertionWithoutUserInterfaceRequest) => void" - )] - assertion_without_user_interface_callback: ThreadsafeFunction< - FnArgs<(u32, u32, PasskeyAssertionWithoutUserInterfaceRequest)>, - >, - #[napi( - ts_arg_type = "(error: null | Error, clientId: number, sequenceNumber: number, message: NativeStatus) => void" - )] - native_status_callback: ThreadsafeFunction<(u32, u32, NativeStatus)>, - ) -> napi::Result { - let (send, mut recv) = tokio::sync::mpsc::channel::(32); - tokio::spawn(async move { - while let Some(Message { - client_id, - kind, - message, - }) = recv.recv().await - { - match kind { - // TODO: We're ignoring the connection and disconnection messages for now - MessageType::Connected | MessageType::Disconnected => continue, - MessageType::Message => { - let Some(message) = message else { - error!("Message is empty"); - continue; - }; - - match serde_json::from_str::>( - &message, - ) { - Ok(msg) => { - let value = msg - .value - .map(|value| (client_id, msg.sequence_number, value).into()) - .map_err(|e| napi::Error::from_reason(format!("{e:?}"))); - - assertion_callback - .call(value, ThreadsafeFunctionCallMode::NonBlocking); - continue; - } - Err(e) => { - error!(error = %e, "Error deserializing message1"); - } - } - - match serde_json::from_str::< - PasskeyMessage, - >(&message) - { - Ok(msg) => { - let value = msg - .value - .map(|value| (client_id, msg.sequence_number, value).into()) - .map_err(|e| napi::Error::from_reason(format!("{e:?}"))); - - assertion_without_user_interface_callback - .call(value, ThreadsafeFunctionCallMode::NonBlocking); - continue; - } - Err(e) => { - error!(error = %e, "Error deserializing message1"); - } - } - - match serde_json::from_str::>( - &message, - ) { - Ok(msg) => { - let value = msg - .value - .map(|value| (client_id, msg.sequence_number, value).into()) - .map_err(|e| napi::Error::from_reason(format!("{e:?}"))); - registration_callback - .call(value, ThreadsafeFunctionCallMode::NonBlocking); - continue; - } - Err(e) => { - error!(error = %e, "Error deserializing message2"); - } - } - - match serde_json::from_str::>(&message) { - Ok(msg) => { - let value = msg - .value - .map(|value| (client_id, msg.sequence_number, value)) - .map_err(|e| napi::Error::from_reason(format!("{e:?}"))); - native_status_callback - .call(value, ThreadsafeFunctionCallMode::NonBlocking); - continue; - } - Err(error) => { - error!(%error, "Unable to deserialze native status."); - } - } - - error!(message, "Received an unknown message2"); - } - } - } - }); - - let path = desktop_core::ipc::path(&name); - - let server = desktop_core::ipc::server::Server::start(&path, send).map_err(|e| { - napi::Error::from_reason(format!( - "Error listening to server - Path: {path:?} - Error: {e} - {e:?}" - )) - })?; - - Ok(AutofillIpcServer { server }) - } - - /// Return the path to the IPC server. - #[napi] - pub fn get_path(&self) -> String { - self.server.path.to_string_lossy().to_string() - } - - /// Stop the IPC server. - #[napi] - pub fn stop(&self) -> napi::Result<()> { - self.server.stop(); - Ok(()) - } - - #[napi] - pub fn complete_registration( - &self, - client_id: u32, - sequence_number: u32, - response: PasskeyRegistrationResponse, - ) -> napi::Result { - let message = PasskeyMessage { - sequence_number, - value: Ok(response), - }; - self.send(client_id, serde_json::to_string(&message).unwrap()) - } - - #[napi] - pub fn complete_assertion( - &self, - client_id: u32, - sequence_number: u32, - response: PasskeyAssertionResponse, - ) -> napi::Result { - let message = PasskeyMessage { - sequence_number, - value: Ok(response), - }; - self.send(client_id, serde_json::to_string(&message).unwrap()) - } - - #[napi] - pub fn complete_error( - &self, - client_id: u32, - sequence_number: u32, - error: String, - ) -> napi::Result { - let message: PasskeyMessage<()> = PasskeyMessage { - sequence_number, - value: Err(BitwardenError::Internal(error)), - }; - self.send(client_id, serde_json::to_string(&message).unwrap()) - } - - // TODO: Add a way to send a message to a specific client? - fn send(&self, _client_id: u32, message: String) -> napi::Result { - self.server - .send(message) - .map_err(|e| { - napi::Error::from_reason(format!("Error sending message - Error: {e} - {e:?}")) - }) - // NAPI doesn't support u64 or usize, so we need to convert to u32 - .map(|u| u32::try_from(u).unwrap_or_default()) - } - } -} diff --git a/apps/desktop/desktop_native/napi/src/autostart.rs b/apps/desktop/desktop_native/napi/src/autostart.rs deleted file mode 100644 index 3068226809e..00000000000 --- a/apps/desktop/desktop_native/napi/src/autostart.rs +++ /dev/null @@ -1,9 +0,0 @@ -#[napi] -pub mod autostart { - #[napi] - pub async fn set_autostart(autostart: bool, params: Vec) -> napi::Result<()> { - desktop_core::autostart::set_autostart(autostart, params) - .await - .map_err(|e| napi::Error::from_reason(format!("Error setting autostart - {e} - {e:?}"))) - } -} diff --git a/apps/desktop/desktop_native/napi/src/autotype.rs b/apps/desktop/desktop_native/napi/src/autotype.rs deleted file mode 100644 index b63c95ceb5c..00000000000 --- a/apps/desktop/desktop_native/napi/src/autotype.rs +++ /dev/null @@ -1,20 +0,0 @@ -#[napi] -pub mod autotype { - #[napi] - pub fn get_foreground_window_title() -> napi::Result { - autotype::get_foreground_window_title().map_err(|_| { - napi::Error::from_reason( - "Autotype Error: failed to get foreground window title".to_string(), - ) - }) - } - - #[napi] - pub fn type_input( - input: Vec, - keyboard_shortcut: Vec, - ) -> napi::Result<(), napi::Status> { - autotype::type_input(&input, &keyboard_shortcut) - .map_err(|e| napi::Error::from_reason(format!("Autotype Error: {e}"))) - } -} diff --git a/apps/desktop/desktop_native/napi/src/biometrics.rs b/apps/desktop/desktop_native/napi/src/biometrics.rs deleted file mode 100644 index bca802d5884..00000000000 --- a/apps/desktop/desktop_native/napi/src/biometrics.rs +++ /dev/null @@ -1,100 +0,0 @@ -#[napi] -pub mod biometrics { - use desktop_core::biometric::{Biometric, BiometricTrait}; - - // Prompt for biometric confirmation - #[napi] - pub async fn prompt( - hwnd: napi::bindgen_prelude::Buffer, - message: String, - ) -> napi::Result { - Biometric::prompt(hwnd.into(), message) - .await - .map_err(|e| napi::Error::from_reason(e.to_string())) - } - - #[napi] - pub async fn available() -> napi::Result { - Biometric::available() - .await - .map_err(|e| napi::Error::from_reason(e.to_string())) - } - - #[napi] - pub async fn set_biometric_secret( - service: String, - account: String, - secret: String, - key_material: Option, - iv_b64: String, - ) -> napi::Result { - Biometric::set_biometric_secret( - &service, - &account, - &secret, - key_material.map(|m| m.into()), - &iv_b64, - ) - .await - .map_err(|e| napi::Error::from_reason(e.to_string())) - } - - /// Retrieves the biometric secret for the given service and account. - /// Throws Error with message [`passwords::PASSWORD_NOT_FOUND`] if the secret does not exist. - #[napi] - pub async fn get_biometric_secret( - service: String, - account: String, - key_material: Option, - ) -> napi::Result { - Biometric::get_biometric_secret(&service, &account, key_material.map(|m| m.into())) - .await - .map_err(|e| napi::Error::from_reason(e.to_string())) - } - - /// Derives key material from biometric data. Returns a string encoded with a - /// base64 encoded key and the base64 encoded challenge used to create it - /// separated by a `|` character. - /// - /// If the iv is provided, it will be used as the challenge. Otherwise a random challenge will - /// be generated. - /// - /// `format!("|")` - #[allow(clippy::unused_async)] // FIXME: Remove unused async! - #[napi] - pub async fn derive_key_material(iv: Option) -> napi::Result { - Biometric::derive_key_material(iv.as_deref()) - .map(|k| k.into()) - .map_err(|e| napi::Error::from_reason(e.to_string())) - } - - #[napi(object)] - pub struct KeyMaterial { - pub os_key_part_b64: String, - pub client_key_part_b64: Option, - } - - impl From for desktop_core::biometric::KeyMaterial { - fn from(km: KeyMaterial) -> Self { - desktop_core::biometric::KeyMaterial { - os_key_part_b64: km.os_key_part_b64, - client_key_part_b64: km.client_key_part_b64, - } - } - } - - #[napi(object)] - pub struct OsDerivedKey { - pub key_b64: String, - pub iv_b64: String, - } - - impl From for OsDerivedKey { - fn from(km: desktop_core::biometric::OsDerivedKey) -> Self { - OsDerivedKey { - key_b64: km.key_b64, - iv_b64: km.iv_b64, - } - } - } -} diff --git a/apps/desktop/desktop_native/napi/src/biometrics_v2.rs b/apps/desktop/desktop_native/napi/src/biometrics_v2.rs deleted file mode 100644 index 2df3a6a07be..00000000000 --- a/apps/desktop/desktop_native/napi/src/biometrics_v2.rs +++ /dev/null @@ -1,116 +0,0 @@ -#[napi] -pub mod biometrics_v2 { - use desktop_core::biometric_v2::BiometricTrait; - - #[napi] - pub struct BiometricLockSystem { - inner: desktop_core::biometric_v2::BiometricLockSystem, - } - - #[napi] - pub fn init_biometric_system() -> napi::Result { - Ok(BiometricLockSystem { - inner: desktop_core::biometric_v2::BiometricLockSystem::new(), - }) - } - - #[napi] - pub async fn authenticate( - biometric_lock_system: &BiometricLockSystem, - hwnd: napi::bindgen_prelude::Buffer, - message: String, - ) -> napi::Result { - biometric_lock_system - .inner - .authenticate(hwnd.into(), message) - .await - .map_err(|e| napi::Error::from_reason(e.to_string())) - } - - #[napi] - pub async fn authenticate_available( - biometric_lock_system: &BiometricLockSystem, - ) -> napi::Result { - biometric_lock_system - .inner - .authenticate_available() - .await - .map_err(|e| napi::Error::from_reason(e.to_string())) - } - - #[napi] - pub async fn enroll_persistent( - biometric_lock_system: &BiometricLockSystem, - user_id: String, - key: napi::bindgen_prelude::Buffer, - ) -> napi::Result<()> { - biometric_lock_system - .inner - .enroll_persistent(&user_id, &key) - .await - .map_err(|e| napi::Error::from_reason(e.to_string())) - } - - #[napi] - pub async fn provide_key( - biometric_lock_system: &BiometricLockSystem, - user_id: String, - key: napi::bindgen_prelude::Buffer, - ) -> napi::Result<()> { - biometric_lock_system - .inner - .provide_key(&user_id, &key) - .await; - Ok(()) - } - - #[napi] - pub async fn unlock( - biometric_lock_system: &BiometricLockSystem, - user_id: String, - hwnd: napi::bindgen_prelude::Buffer, - ) -> napi::Result { - biometric_lock_system - .inner - .unlock(&user_id, hwnd.into()) - .await - .map_err(|e| napi::Error::from_reason(e.to_string())) - .map(|v| v.into()) - } - - #[napi] - pub async fn unlock_available( - biometric_lock_system: &BiometricLockSystem, - user_id: String, - ) -> napi::Result { - biometric_lock_system - .inner - .unlock_available(&user_id) - .await - .map_err(|e| napi::Error::from_reason(e.to_string())) - } - - #[napi] - pub async fn has_persistent( - biometric_lock_system: &BiometricLockSystem, - user_id: String, - ) -> napi::Result { - biometric_lock_system - .inner - .has_persistent(&user_id) - .await - .map_err(|e| napi::Error::from_reason(e.to_string())) - } - - #[napi] - pub async fn unenroll( - biometric_lock_system: &BiometricLockSystem, - user_id: String, - ) -> napi::Result<()> { - biometric_lock_system - .inner - .unenroll(&user_id) - .await - .map_err(|e| napi::Error::from_reason(e.to_string())) - } -} diff --git a/apps/desktop/desktop_native/napi/src/chromium_importer.rs b/apps/desktop/desktop_native/napi/src/chromium_importer.rs deleted file mode 100644 index da295984a47..00000000000 --- a/apps/desktop/desktop_native/napi/src/chromium_importer.rs +++ /dev/null @@ -1,116 +0,0 @@ -#[napi] -pub mod chromium_importer { - use std::collections::HashMap; - - use chromium_importer::{ - chromium::{ - DefaultInstalledBrowserRetriever, LoginImportResult as _LoginImportResult, - ProfileInfo as _ProfileInfo, - }, - metadata::NativeImporterMetadata as _NativeImporterMetadata, - }; - - #[napi(object)] - pub struct ProfileInfo { - pub id: String, - pub name: String, - } - - #[napi(object)] - pub struct Login { - pub url: String, - pub username: String, - pub password: String, - pub note: String, - } - - #[napi(object)] - pub struct LoginImportFailure { - pub url: String, - pub username: String, - pub error: String, - } - - #[napi(object)] - pub struct LoginImportResult { - pub login: Option, - pub failure: Option, - } - - #[napi(object)] - pub struct NativeImporterMetadata { - pub id: String, - pub loaders: Vec, - pub instructions: String, - } - - impl From<_LoginImportResult> for LoginImportResult { - fn from(l: _LoginImportResult) -> Self { - match l { - _LoginImportResult::Success(l) => LoginImportResult { - login: Some(Login { - url: l.url, - username: l.username, - password: l.password, - note: l.note, - }), - failure: None, - }, - _LoginImportResult::Failure(l) => LoginImportResult { - login: None, - failure: Some(LoginImportFailure { - url: l.url, - username: l.username, - error: l.error, - }), - }, - } - } - } - - impl From<_ProfileInfo> for ProfileInfo { - fn from(p: _ProfileInfo) -> Self { - ProfileInfo { - id: p.folder, - name: p.name, - } - } - } - - impl From<_NativeImporterMetadata> for NativeImporterMetadata { - fn from(m: _NativeImporterMetadata) -> Self { - NativeImporterMetadata { - id: m.id, - loaders: m.loaders, - instructions: m.instructions, - } - } - } - - #[napi] - /// Returns OS aware metadata describing supported Chromium based importers as a JSON string. - pub fn get_metadata() -> HashMap { - chromium_importer::metadata::get_supported_importers::() - .into_iter() - .map(|(browser, metadata)| (browser, NativeImporterMetadata::from(metadata))) - .collect() - } - - #[napi] - pub fn get_available_profiles(browser: String) -> napi::Result> { - chromium_importer::chromium::get_available_profiles(&browser) - .map(|profiles| profiles.into_iter().map(ProfileInfo::from).collect()) - .map_err(|e| napi::Error::from_reason(e.to_string())) - } - - #[napi] - pub async fn import_logins( - browser: String, - profile_id: String, - ) -> napi::Result> { - chromium_importer::chromium::import_logins(&browser, &profile_id) - .await - .map(|logins| logins.into_iter().map(LoginImportResult::from).collect()) - .map_err(|e| napi::Error::from_reason(e.to_string())) - } -} diff --git a/apps/desktop/desktop_native/napi/src/clipboards.rs b/apps/desktop/desktop_native/napi/src/clipboards.rs deleted file mode 100644 index 810e457dd60..00000000000 --- a/apps/desktop/desktop_native/napi/src/clipboards.rs +++ /dev/null @@ -1,15 +0,0 @@ -#[napi] -pub mod clipboards { - #[allow(clippy::unused_async)] // FIXME: Remove unused async! - #[napi] - pub async fn read() -> napi::Result { - desktop_core::clipboard::read().map_err(|e| napi::Error::from_reason(e.to_string())) - } - - #[allow(clippy::unused_async)] // FIXME: Remove unused async! - #[napi] - pub async fn write(text: String, password: bool) -> napi::Result<()> { - desktop_core::clipboard::write(&text, password) - .map_err(|e| napi::Error::from_reason(e.to_string())) - } -} diff --git a/apps/desktop/desktop_native/napi/src/ipc.rs b/apps/desktop/desktop_native/napi/src/ipc.rs deleted file mode 100644 index ba72b1dce2b..00000000000 --- a/apps/desktop/desktop_native/napi/src/ipc.rs +++ /dev/null @@ -1,106 +0,0 @@ -#[napi] -pub mod ipc { - use desktop_core::ipc::server::{Message, MessageType}; - use napi::threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode}; - - #[napi(object)] - pub struct IpcMessage { - pub client_id: u32, - pub kind: IpcMessageType, - pub message: Option, - } - - impl From for IpcMessage { - fn from(message: Message) -> Self { - IpcMessage { - client_id: message.client_id, - kind: message.kind.into(), - message: message.message, - } - } - } - - #[napi] - pub enum IpcMessageType { - Connected, - Disconnected, - Message, - } - - impl From for IpcMessageType { - fn from(message_type: MessageType) -> Self { - match message_type { - MessageType::Connected => IpcMessageType::Connected, - MessageType::Disconnected => IpcMessageType::Disconnected, - MessageType::Message => IpcMessageType::Message, - } - } - } - - #[napi] - pub struct NativeIpcServer { - server: desktop_core::ipc::server::Server, - } - - #[napi] - impl NativeIpcServer { - /// Create and start the IPC server without blocking. - /// - /// @param name The endpoint name to listen on. This name uniquely identifies the IPC - /// connection and must be the same for both the server and client. @param callback - /// This function will be called whenever a message is received from a client. - #[allow(clippy::unused_async)] // FIXME: Remove unused async! - #[napi(factory)] - pub async fn listen( - name: String, - #[napi(ts_arg_type = "(error: null | Error, message: IpcMessage) => void")] - callback: ThreadsafeFunction, - ) -> napi::Result { - let (send, mut recv) = tokio::sync::mpsc::channel::(32); - tokio::spawn(async move { - while let Some(message) = recv.recv().await { - callback.call(Ok(message.into()), ThreadsafeFunctionCallMode::NonBlocking); - } - }); - - let path = desktop_core::ipc::path(&name); - - let server = desktop_core::ipc::server::Server::start(&path, send).map_err(|e| { - napi::Error::from_reason(format!( - "Error listening to server - Path: {path:?} - Error: {e} - {e:?}" - )) - })?; - - Ok(NativeIpcServer { server }) - } - - /// Return the path to the IPC server. - #[napi] - pub fn get_path(&self) -> String { - self.server.path.to_string_lossy().to_string() - } - - /// Stop the IPC server. - #[napi] - pub fn stop(&self) -> napi::Result<()> { - self.server.stop(); - Ok(()) - } - - /// Send a message over the IPC server to all the connected clients - /// - /// @return The number of clients that the message was sent to. Note that the number of - /// messages actually received may be less, as some clients could disconnect before - /// receiving the message. - #[napi] - pub fn send(&self, message: String) -> napi::Result { - self.server - .send(message) - .map_err(|e| { - napi::Error::from_reason(format!("Error sending message - Error: {e} - {e:?}")) - }) - // NAPI doesn't support u64 or usize, so we need to convert to u32 - .map(|u| u32::try_from(u).unwrap_or_default()) - } - } -} diff --git a/apps/desktop/desktop_native/napi/src/lib.rs b/apps/desktop/desktop_native/napi/src/lib.rs index e3abfd50e7a..588f757631c 100644 --- a/apps/desktop/desktop_native/napi/src/lib.rs +++ b/apps/desktop/desktop_native/napi/src/lib.rs @@ -4,22 +4,1244 @@ extern crate napi_derive; mod passkey_authenticator_internal; mod registry; -// NAPI namespaces -// In each of these modules, the types are defined within a nested namespace of -// the same name so that NAPI can export the TypeScript types within a -// namespace. -pub mod autofill; -pub mod autostart; -pub mod autotype; -pub mod biometrics; -pub mod biometrics_v2; -pub mod chromium_importer; -pub mod clipboards; -pub mod ipc; -pub mod logging; -pub mod passkey_authenticator; -pub mod passwords; -pub mod powermonitors; -pub mod processisolations; -pub mod sshagent; -pub mod windows_registry; +#[napi] +pub mod passwords { + /// The error message returned when a password is not found during retrieval or deletion. + #[napi] + pub const PASSWORD_NOT_FOUND: &str = desktop_core::password::PASSWORD_NOT_FOUND; + + /// Fetch the stored password from the keychain. + /// Throws {@link Error} with message {@link PASSWORD_NOT_FOUND} if the password does not exist. + #[napi] + pub async fn get_password(service: String, account: String) -> napi::Result { + desktop_core::password::get_password(&service, &account) + .await + .map_err(|e| napi::Error::from_reason(e.to_string())) + } + + /// Save the password to the keychain. Adds an entry if none exists otherwise updates the + /// existing entry. + #[napi] + pub async fn set_password( + service: String, + account: String, + password: String, + ) -> napi::Result<()> { + desktop_core::password::set_password(&service, &account, &password) + .await + .map_err(|e| napi::Error::from_reason(e.to_string())) + } + + /// Delete the stored password from the keychain. + /// Throws {@link Error} with message {@link PASSWORD_NOT_FOUND} if the password does not exist. + #[napi] + pub async fn delete_password(service: String, account: String) -> napi::Result<()> { + desktop_core::password::delete_password(&service, &account) + .await + .map_err(|e| napi::Error::from_reason(e.to_string())) + } + + /// Checks if the os secure storage is available + #[napi] + pub async fn is_available() -> napi::Result { + desktop_core::password::is_available() + .await + .map_err(|e| napi::Error::from_reason(e.to_string())) + } +} + +#[napi] +pub mod biometrics { + use desktop_core::biometric::{Biometric, BiometricTrait}; + + // Prompt for biometric confirmation + #[napi] + pub async fn prompt( + hwnd: napi::bindgen_prelude::Buffer, + message: String, + ) -> napi::Result { + Biometric::prompt(hwnd.into(), message) + .await + .map_err(|e| napi::Error::from_reason(e.to_string())) + } + + #[napi] + pub async fn available() -> napi::Result { + Biometric::available() + .await + .map_err(|e| napi::Error::from_reason(e.to_string())) + } + + #[napi] + pub async fn set_biometric_secret( + service: String, + account: String, + secret: String, + key_material: Option, + iv_b64: String, + ) -> napi::Result { + Biometric::set_biometric_secret( + &service, + &account, + &secret, + key_material.map(|m| m.into()), + &iv_b64, + ) + .await + .map_err(|e| napi::Error::from_reason(e.to_string())) + } + + /// Retrieves the biometric secret for the given service and account. + /// Throws Error with message [`passwords::PASSWORD_NOT_FOUND`] if the secret does not exist. + #[napi] + pub async fn get_biometric_secret( + service: String, + account: String, + key_material: Option, + ) -> napi::Result { + Biometric::get_biometric_secret(&service, &account, key_material.map(|m| m.into())) + .await + .map_err(|e| napi::Error::from_reason(e.to_string())) + } + + /// Derives key material from biometric data. Returns a string encoded with a + /// base64 encoded key and the base64 encoded challenge used to create it + /// separated by a `|` character. + /// + /// If the iv is provided, it will be used as the challenge. Otherwise a random challenge will + /// be generated. + /// + /// `format!("|")` + #[allow(clippy::unused_async)] // FIXME: Remove unused async! + #[napi] + pub async fn derive_key_material(iv: Option) -> napi::Result { + Biometric::derive_key_material(iv.as_deref()) + .map(|k| k.into()) + .map_err(|e| napi::Error::from_reason(e.to_string())) + } + + #[napi(object)] + pub struct KeyMaterial { + pub os_key_part_b64: String, + pub client_key_part_b64: Option, + } + + impl From for desktop_core::biometric::KeyMaterial { + fn from(km: KeyMaterial) -> Self { + desktop_core::biometric::KeyMaterial { + os_key_part_b64: km.os_key_part_b64, + client_key_part_b64: km.client_key_part_b64, + } + } + } + + #[napi(object)] + pub struct OsDerivedKey { + pub key_b64: String, + pub iv_b64: String, + } + + impl From for OsDerivedKey { + fn from(km: desktop_core::biometric::OsDerivedKey) -> Self { + OsDerivedKey { + key_b64: km.key_b64, + iv_b64: km.iv_b64, + } + } + } +} + +#[napi] +pub mod biometrics_v2 { + use desktop_core::biometric_v2::BiometricTrait; + + #[napi] + pub struct BiometricLockSystem { + inner: desktop_core::biometric_v2::BiometricLockSystem, + } + + #[napi] + pub fn init_biometric_system() -> napi::Result { + Ok(BiometricLockSystem { + inner: desktop_core::biometric_v2::BiometricLockSystem::new(), + }) + } + + #[napi] + pub async fn authenticate( + biometric_lock_system: &BiometricLockSystem, + hwnd: napi::bindgen_prelude::Buffer, + message: String, + ) -> napi::Result { + biometric_lock_system + .inner + .authenticate(hwnd.into(), message) + .await + .map_err(|e| napi::Error::from_reason(e.to_string())) + } + + #[napi] + pub async fn authenticate_available( + biometric_lock_system: &BiometricLockSystem, + ) -> napi::Result { + biometric_lock_system + .inner + .authenticate_available() + .await + .map_err(|e| napi::Error::from_reason(e.to_string())) + } + + #[napi] + pub async fn enroll_persistent( + biometric_lock_system: &BiometricLockSystem, + user_id: String, + key: napi::bindgen_prelude::Buffer, + ) -> napi::Result<()> { + biometric_lock_system + .inner + .enroll_persistent(&user_id, &key) + .await + .map_err(|e| napi::Error::from_reason(e.to_string())) + } + + #[napi] + pub async fn provide_key( + biometric_lock_system: &BiometricLockSystem, + user_id: String, + key: napi::bindgen_prelude::Buffer, + ) -> napi::Result<()> { + biometric_lock_system + .inner + .provide_key(&user_id, &key) + .await; + Ok(()) + } + + #[napi] + pub async fn unlock( + biometric_lock_system: &BiometricLockSystem, + user_id: String, + hwnd: napi::bindgen_prelude::Buffer, + ) -> napi::Result { + biometric_lock_system + .inner + .unlock(&user_id, hwnd.into()) + .await + .map_err(|e| napi::Error::from_reason(e.to_string())) + .map(|v| v.into()) + } + + #[napi] + pub async fn unlock_available( + biometric_lock_system: &BiometricLockSystem, + user_id: String, + ) -> napi::Result { + biometric_lock_system + .inner + .unlock_available(&user_id) + .await + .map_err(|e| napi::Error::from_reason(e.to_string())) + } + + #[napi] + pub async fn has_persistent( + biometric_lock_system: &BiometricLockSystem, + user_id: String, + ) -> napi::Result { + biometric_lock_system + .inner + .has_persistent(&user_id) + .await + .map_err(|e| napi::Error::from_reason(e.to_string())) + } + + #[napi] + pub async fn unenroll( + biometric_lock_system: &BiometricLockSystem, + user_id: String, + ) -> napi::Result<()> { + biometric_lock_system + .inner + .unenroll(&user_id) + .await + .map_err(|e| napi::Error::from_reason(e.to_string())) + } +} + +#[napi] +pub mod clipboards { + #[allow(clippy::unused_async)] // FIXME: Remove unused async! + #[napi] + pub async fn read() -> napi::Result { + desktop_core::clipboard::read().map_err(|e| napi::Error::from_reason(e.to_string())) + } + + #[allow(clippy::unused_async)] // FIXME: Remove unused async! + #[napi] + pub async fn write(text: String, password: bool) -> napi::Result<()> { + desktop_core::clipboard::write(&text, password) + .map_err(|e| napi::Error::from_reason(e.to_string())) + } +} + +#[napi] +pub mod sshagent { + use std::sync::Arc; + + use napi::{ + bindgen_prelude::Promise, + threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode}, + }; + use tokio::{self, sync::Mutex}; + use tracing::error; + + #[napi] + pub struct SshAgentState { + state: desktop_core::ssh_agent::BitwardenDesktopAgent, + } + + #[napi(object)] + pub struct PrivateKey { + pub private_key: String, + pub name: String, + pub cipher_id: String, + } + + #[napi(object)] + pub struct SshKey { + pub private_key: String, + pub public_key: String, + pub key_fingerprint: String, + } + + #[napi(object)] + pub struct SshUIRequest { + pub cipher_id: Option, + pub is_list: bool, + pub process_name: String, + pub is_forwarding: bool, + pub namespace: Option, + } + + #[allow(clippy::unused_async)] // FIXME: Remove unused async! + #[napi] + pub async fn serve( + callback: ThreadsafeFunction>, + ) -> napi::Result { + let (auth_request_tx, mut auth_request_rx) = + tokio::sync::mpsc::channel::(32); + let (auth_response_tx, auth_response_rx) = + tokio::sync::broadcast::channel::<(u32, bool)>(32); + let auth_response_tx_arc = Arc::new(Mutex::new(auth_response_tx)); + // Wrap callback in Arc so it can be shared across spawned tasks + let callback = Arc::new(callback); + tokio::spawn(async move { + let _ = auth_response_rx; + + while let Some(request) = auth_request_rx.recv().await { + let cloned_response_tx_arc = auth_response_tx_arc.clone(); + let cloned_callback = callback.clone(); + tokio::spawn(async move { + let auth_response_tx_arc = cloned_response_tx_arc; + let callback = cloned_callback; + // In NAPI v3, obtain the JS callback return as a Promise and await it + // in Rust + let (tx, rx) = std::sync::mpsc::channel::>(); + let status = callback.call_with_return_value( + Ok(SshUIRequest { + cipher_id: request.cipher_id, + is_list: request.is_list, + process_name: request.process_name, + is_forwarding: request.is_forwarding, + namespace: request.namespace, + }), + ThreadsafeFunctionCallMode::Blocking, + move |ret: Result, napi::Error>, _env| { + if let Ok(p) = ret { + let _ = tx.send(p); + } + Ok(()) + }, + ); + + let result = if status == napi::Status::Ok { + match rx.recv() { + Ok(promise) => match promise.await { + Ok(v) => v, + Err(e) => { + error!(error = %e, "UI callback promise rejected"); + false + } + }, + Err(e) => { + error!(error = %e, "Failed to receive UI callback promise"); + false + } + } + } else { + error!(error = ?status, "Calling UI callback failed"); + false + }; + + let _ = auth_response_tx_arc + .lock() + .await + .send((request.request_id, result)) + .expect("should be able to send auth response to agent"); + }); + } + }); + + match desktop_core::ssh_agent::BitwardenDesktopAgent::start_server( + auth_request_tx, + Arc::new(Mutex::new(auth_response_rx)), + ) { + Ok(state) => Ok(SshAgentState { state }), + Err(e) => Err(napi::Error::from_reason(e.to_string())), + } + } + + #[napi] + pub fn stop(agent_state: &mut SshAgentState) -> napi::Result<()> { + let bitwarden_agent_state = &mut agent_state.state; + bitwarden_agent_state.stop(); + Ok(()) + } + + #[napi] + pub fn is_running(agent_state: &mut SshAgentState) -> bool { + let bitwarden_agent_state = agent_state.state.clone(); + bitwarden_agent_state.is_running() + } + + #[napi] + pub fn set_keys( + agent_state: &mut SshAgentState, + new_keys: Vec, + ) -> napi::Result<()> { + let bitwarden_agent_state = &mut agent_state.state; + bitwarden_agent_state + .set_keys( + new_keys + .iter() + .map(|k| (k.private_key.clone(), k.name.clone(), k.cipher_id.clone())) + .collect(), + ) + .map_err(|e| napi::Error::from_reason(e.to_string()))?; + Ok(()) + } + + #[napi] + pub fn lock(agent_state: &mut SshAgentState) -> napi::Result<()> { + let bitwarden_agent_state = &mut agent_state.state; + bitwarden_agent_state + .lock() + .map_err(|e| napi::Error::from_reason(e.to_string())) + } + + #[napi] + pub fn clear_keys(agent_state: &mut SshAgentState) -> napi::Result<()> { + let bitwarden_agent_state = &mut agent_state.state; + bitwarden_agent_state + .clear_keys() + .map_err(|e| napi::Error::from_reason(e.to_string())) + } +} + +#[napi] +pub mod processisolations { + #[allow(clippy::unused_async)] // FIXME: Remove unused async! + #[napi] + pub async fn disable_coredumps() -> napi::Result<()> { + desktop_core::process_isolation::disable_coredumps() + .map_err(|e| napi::Error::from_reason(e.to_string())) + } + + #[allow(clippy::unused_async)] // FIXME: Remove unused async! + #[napi] + pub async fn is_core_dumping_disabled() -> napi::Result { + desktop_core::process_isolation::is_core_dumping_disabled() + .map_err(|e| napi::Error::from_reason(e.to_string())) + } + + #[allow(clippy::unused_async)] // FIXME: Remove unused async! + #[napi] + pub async fn isolate_process() -> napi::Result<()> { + desktop_core::process_isolation::isolate_process() + .map_err(|e| napi::Error::from_reason(e.to_string())) + } +} + +#[napi] +pub mod powermonitors { + use napi::{ + threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode}, + tokio, + }; + + #[napi] + pub async fn on_lock(callback: ThreadsafeFunction<()>) -> napi::Result<()> { + let (tx, mut rx) = tokio::sync::mpsc::channel::<()>(32); + desktop_core::powermonitor::on_lock(tx) + .await + .map_err(|e| napi::Error::from_reason(e.to_string()))?; + tokio::spawn(async move { + while let Some(()) = rx.recv().await { + callback.call(Ok(()), ThreadsafeFunctionCallMode::NonBlocking); + } + }); + Ok(()) + } + + #[napi] + pub async fn is_lock_monitor_available() -> napi::Result { + Ok(desktop_core::powermonitor::is_lock_monitor_available().await) + } +} + +#[napi] +pub mod windows_registry { + #[allow(clippy::unused_async)] // FIXME: Remove unused async! + #[napi] + pub async fn create_key(key: String, subkey: String, value: String) -> napi::Result<()> { + crate::registry::create_key(&key, &subkey, &value) + .map_err(|e| napi::Error::from_reason(e.to_string())) + } + + #[allow(clippy::unused_async)] // FIXME: Remove unused async! + #[napi] + pub async fn delete_key(key: String, subkey: String) -> napi::Result<()> { + crate::registry::delete_key(&key, &subkey) + .map_err(|e| napi::Error::from_reason(e.to_string())) + } +} + +#[napi] +pub mod ipc { + use desktop_core::ipc::server::{Message, MessageType}; + use napi::threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode}; + + #[napi(object)] + pub struct IpcMessage { + pub client_id: u32, + pub kind: IpcMessageType, + pub message: Option, + } + + impl From for IpcMessage { + fn from(message: Message) -> Self { + IpcMessage { + client_id: message.client_id, + kind: message.kind.into(), + message: message.message, + } + } + } + + #[napi] + pub enum IpcMessageType { + Connected, + Disconnected, + Message, + } + + impl From for IpcMessageType { + fn from(message_type: MessageType) -> Self { + match message_type { + MessageType::Connected => IpcMessageType::Connected, + MessageType::Disconnected => IpcMessageType::Disconnected, + MessageType::Message => IpcMessageType::Message, + } + } + } + + #[napi] + pub struct NativeIpcServer { + server: desktop_core::ipc::server::Server, + } + + #[napi] + impl NativeIpcServer { + /// Create and start the IPC server without blocking. + /// + /// @param name The endpoint name to listen on. This name uniquely identifies the IPC + /// connection and must be the same for both the server and client. @param callback + /// This function will be called whenever a message is received from a client. + #[allow(clippy::unused_async)] // FIXME: Remove unused async! + #[napi(factory)] + pub async fn listen( + name: String, + #[napi(ts_arg_type = "(error: null | Error, message: IpcMessage) => void")] + callback: ThreadsafeFunction, + ) -> napi::Result { + let (send, mut recv) = tokio::sync::mpsc::channel::(32); + tokio::spawn(async move { + while let Some(message) = recv.recv().await { + callback.call(Ok(message.into()), ThreadsafeFunctionCallMode::NonBlocking); + } + }); + + let path = desktop_core::ipc::path(&name); + + let server = desktop_core::ipc::server::Server::start(&path, send).map_err(|e| { + napi::Error::from_reason(format!( + "Error listening to server - Path: {path:?} - Error: {e} - {e:?}" + )) + })?; + + Ok(NativeIpcServer { server }) + } + + /// Return the path to the IPC server. + #[napi] + pub fn get_path(&self) -> String { + self.server.path.to_string_lossy().to_string() + } + + /// Stop the IPC server. + #[napi] + pub fn stop(&self) -> napi::Result<()> { + self.server.stop(); + Ok(()) + } + + /// Send a message over the IPC server to all the connected clients + /// + /// @return The number of clients that the message was sent to. Note that the number of + /// messages actually received may be less, as some clients could disconnect before + /// receiving the message. + #[napi] + pub fn send(&self, message: String) -> napi::Result { + self.server + .send(message) + .map_err(|e| { + napi::Error::from_reason(format!("Error sending message - Error: {e} - {e:?}")) + }) + // NAPI doesn't support u64 or usize, so we need to convert to u32 + .map(|u| u32::try_from(u).unwrap_or_default()) + } + } +} + +#[napi] +pub mod autostart { + #[napi] + pub async fn set_autostart(autostart: bool, params: Vec) -> napi::Result<()> { + desktop_core::autostart::set_autostart(autostart, params) + .await + .map_err(|e| napi::Error::from_reason(format!("Error setting autostart - {e} - {e:?}"))) + } +} + +#[napi] +pub mod autofill { + use desktop_core::ipc::server::{Message, MessageType}; + use napi::{ + bindgen_prelude::FnArgs, + threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode}, + }; + use serde::{de::DeserializeOwned, Deserialize, Serialize}; + use tracing::error; + + #[napi] + pub async fn run_command(value: String) -> napi::Result { + desktop_core::autofill::run_command(value) + .await + .map_err(|e| napi::Error::from_reason(e.to_string())) + } + + #[derive(Debug, serde::Serialize, serde:: Deserialize)] + pub enum BitwardenError { + Internal(String), + } + + #[napi(string_enum)] + #[derive(Debug, Serialize, Deserialize)] + #[serde(rename_all = "camelCase")] + pub enum UserVerification { + #[napi(value = "preferred")] + Preferred, + #[napi(value = "required")] + Required, + #[napi(value = "discouraged")] + Discouraged, + } + + #[derive(Serialize, Deserialize)] + #[serde(bound = "T: Serialize + DeserializeOwned")] + pub struct PasskeyMessage { + pub sequence_number: u32, + pub value: Result, + } + + #[napi(object)] + #[derive(Debug, Serialize, Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct Position { + pub x: i32, + pub y: i32, + } + + #[napi(object)] + #[derive(Debug, Serialize, Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct PasskeyRegistrationRequest { + pub rp_id: String, + pub user_name: String, + pub user_handle: Vec, + pub client_data_hash: Vec, + pub user_verification: UserVerification, + pub supported_algorithms: Vec, + pub window_xy: Position, + pub excluded_credentials: Vec>, + } + + #[napi(object)] + #[derive(Serialize, Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct PasskeyRegistrationResponse { + pub rp_id: String, + pub client_data_hash: Vec, + pub credential_id: Vec, + pub attestation_object: Vec, + } + + #[napi(object)] + #[derive(Debug, Serialize, Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct PasskeyAssertionRequest { + pub rp_id: String, + pub client_data_hash: Vec, + pub user_verification: UserVerification, + pub allowed_credentials: Vec>, + pub window_xy: Position, + //extension_input: Vec, TODO: Implement support for extensions + } + + #[napi(object)] + #[derive(Debug, Serialize, Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct PasskeyAssertionWithoutUserInterfaceRequest { + pub rp_id: String, + pub credential_id: Vec, + pub user_name: String, + pub user_handle: Vec, + pub record_identifier: Option, + pub client_data_hash: Vec, + pub user_verification: UserVerification, + pub window_xy: Position, + } + + #[napi(object)] + #[derive(Debug, Serialize, Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct NativeStatus { + pub key: String, + pub value: String, + } + + #[napi(object)] + #[derive(Serialize, Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct PasskeyAssertionResponse { + pub rp_id: String, + pub user_handle: Vec, + pub signature: Vec, + pub client_data_hash: Vec, + pub authenticator_data: Vec, + pub credential_id: Vec, + } + + #[napi] + pub struct AutofillIpcServer { + server: desktop_core::ipc::server::Server, + } + + // FIXME: Remove unwraps! They panic and terminate the whole application. + #[allow(clippy::unwrap_used)] + #[napi] + impl AutofillIpcServer { + /// Create and start the IPC server without blocking. + /// + /// @param name The endpoint name to listen on. This name uniquely identifies the IPC + /// connection and must be the same for both the server and client. @param callback + /// This function will be called whenever a message is received from a client. + #[allow(clippy::unused_async)] // FIXME: Remove unused async! + #[napi(factory)] + pub async fn listen( + name: String, + // Ideally we'd have a single callback that has an enum containing the request values, + // but NAPI doesn't support that just yet + #[napi( + ts_arg_type = "(error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyRegistrationRequest) => void" + )] + registration_callback: ThreadsafeFunction< + FnArgs<(u32, u32, PasskeyRegistrationRequest)>, + >, + #[napi( + ts_arg_type = "(error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyAssertionRequest) => void" + )] + assertion_callback: ThreadsafeFunction< + FnArgs<(u32, u32, PasskeyAssertionRequest)>, + >, + #[napi( + ts_arg_type = "(error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyAssertionWithoutUserInterfaceRequest) => void" + )] + assertion_without_user_interface_callback: ThreadsafeFunction< + FnArgs<(u32, u32, PasskeyAssertionWithoutUserInterfaceRequest)>, + >, + #[napi( + ts_arg_type = "(error: null | Error, clientId: number, sequenceNumber: number, message: NativeStatus) => void" + )] + native_status_callback: ThreadsafeFunction<(u32, u32, NativeStatus)>, + ) -> napi::Result { + let (send, mut recv) = tokio::sync::mpsc::channel::(32); + tokio::spawn(async move { + while let Some(Message { + client_id, + kind, + message, + }) = recv.recv().await + { + match kind { + // TODO: We're ignoring the connection and disconnection messages for now + MessageType::Connected | MessageType::Disconnected => continue, + MessageType::Message => { + let Some(message) = message else { + error!("Message is empty"); + continue; + }; + + match serde_json::from_str::>( + &message, + ) { + Ok(msg) => { + let value = msg + .value + .map(|value| (client_id, msg.sequence_number, value).into()) + .map_err(|e| napi::Error::from_reason(format!("{e:?}"))); + + assertion_callback + .call(value, ThreadsafeFunctionCallMode::NonBlocking); + continue; + } + Err(e) => { + error!(error = %e, "Error deserializing message1"); + } + } + + match serde_json::from_str::< + PasskeyMessage, + >(&message) + { + Ok(msg) => { + let value = msg + .value + .map(|value| (client_id, msg.sequence_number, value).into()) + .map_err(|e| napi::Error::from_reason(format!("{e:?}"))); + + assertion_without_user_interface_callback + .call(value, ThreadsafeFunctionCallMode::NonBlocking); + continue; + } + Err(e) => { + error!(error = %e, "Error deserializing message1"); + } + } + + match serde_json::from_str::>( + &message, + ) { + Ok(msg) => { + let value = msg + .value + .map(|value| (client_id, msg.sequence_number, value).into()) + .map_err(|e| napi::Error::from_reason(format!("{e:?}"))); + registration_callback + .call(value, ThreadsafeFunctionCallMode::NonBlocking); + continue; + } + Err(e) => { + error!(error = %e, "Error deserializing message2"); + } + } + + match serde_json::from_str::>(&message) { + Ok(msg) => { + let value = msg + .value + .map(|value| (client_id, msg.sequence_number, value)) + .map_err(|e| napi::Error::from_reason(format!("{e:?}"))); + native_status_callback + .call(value, ThreadsafeFunctionCallMode::NonBlocking); + continue; + } + Err(error) => { + error!(%error, "Unable to deserialze native status."); + } + } + + error!(message, "Received an unknown message2"); + } + } + } + }); + + let path = desktop_core::ipc::path(&name); + + let server = desktop_core::ipc::server::Server::start(&path, send).map_err(|e| { + napi::Error::from_reason(format!( + "Error listening to server - Path: {path:?} - Error: {e} - {e:?}" + )) + })?; + + Ok(AutofillIpcServer { server }) + } + + /// Return the path to the IPC server. + #[napi] + pub fn get_path(&self) -> String { + self.server.path.to_string_lossy().to_string() + } + + /// Stop the IPC server. + #[napi] + pub fn stop(&self) -> napi::Result<()> { + self.server.stop(); + Ok(()) + } + + #[napi] + pub fn complete_registration( + &self, + client_id: u32, + sequence_number: u32, + response: PasskeyRegistrationResponse, + ) -> napi::Result { + let message = PasskeyMessage { + sequence_number, + value: Ok(response), + }; + self.send(client_id, serde_json::to_string(&message).unwrap()) + } + + #[napi] + pub fn complete_assertion( + &self, + client_id: u32, + sequence_number: u32, + response: PasskeyAssertionResponse, + ) -> napi::Result { + let message = PasskeyMessage { + sequence_number, + value: Ok(response), + }; + self.send(client_id, serde_json::to_string(&message).unwrap()) + } + + #[napi] + pub fn complete_error( + &self, + client_id: u32, + sequence_number: u32, + error: String, + ) -> napi::Result { + let message: PasskeyMessage<()> = PasskeyMessage { + sequence_number, + value: Err(BitwardenError::Internal(error)), + }; + self.send(client_id, serde_json::to_string(&message).unwrap()) + } + + // TODO: Add a way to send a message to a specific client? + fn send(&self, _client_id: u32, message: String) -> napi::Result { + self.server + .send(message) + .map_err(|e| { + napi::Error::from_reason(format!("Error sending message - Error: {e} - {e:?}")) + }) + // NAPI doesn't support u64 or usize, so we need to convert to u32 + .map(|u| u32::try_from(u).unwrap_or_default()) + } + } +} + +#[napi] +pub mod passkey_authenticator { + #[napi] + pub fn register() -> napi::Result<()> { + crate::passkey_authenticator_internal::register().map_err(|e| { + napi::Error::from_reason(format!("Passkey registration failed - Error: {e} - {e:?}")) + }) + } +} + +#[napi] +pub mod logging { + //! `logging` is the interface between the native desktop's usage of the `tracing` crate + //! for logging, to intercept events and write to the JS space. + //! + //! # Example + //! + //! [Elec] 14:34:03.517 › [NAPI] [INFO] desktop_core::ssh_agent::platform_ssh_agent: Starting + //! SSH Agent server {socket=/Users/foo/.bitwarden-ssh-agent.sock} + + use std::{fmt::Write, sync::OnceLock}; + + use napi::{ + bindgen_prelude::FnArgs, + threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode}, + }; + use tracing::Level; + use tracing_subscriber::{ + filter::EnvFilter, + fmt::format::{DefaultVisitor, Writer}, + layer::SubscriberExt, + util::SubscriberInitExt, + Layer, + }; + + struct JsLogger(OnceLock>>); + static JS_LOGGER: JsLogger = JsLogger(OnceLock::new()); + + #[napi] + pub enum LogLevel { + Trace, + Debug, + Info, + Warn, + Error, + } + + impl From<&Level> for LogLevel { + fn from(level: &Level) -> Self { + match *level { + Level::TRACE => LogLevel::Trace, + Level::DEBUG => LogLevel::Debug, + Level::INFO => LogLevel::Info, + Level::WARN => LogLevel::Warn, + Level::ERROR => LogLevel::Error, + } + } + } + + // JsLayer lets us intercept events and write them to the JS Logger. + struct JsLayer; + + impl Layer for JsLayer + where + S: tracing::Subscriber, + { + // This function builds a log message buffer from the event data and + // calls the JS logger with it. + // + // For example, this log call: + // + // ``` + // mod supreme { + // mod module { + // let foo = "bar"; + // info!(best_variable_name = %foo, "Foo done it again."); + // } + // } + // ``` + // + // , results in the following string: + // + // [INFO] supreme::module: Foo done it again. {best_variable_name=bar} + fn on_event( + &self, + event: &tracing::Event<'_>, + _ctx: tracing_subscriber::layer::Context<'_, S>, + ) { + let mut buffer = String::new(); + + // create the preamble text that precedes the message and vars. e.g.: + // [INFO] desktop_core::ssh_agent::platform_ssh_agent: + let level = event.metadata().level().as_str(); + let module_path = event.metadata().module_path().unwrap_or_default(); + + write!(&mut buffer, "[{level}] {module_path}:") + .expect("Failed to write tracing event to buffer"); + + let writer = Writer::new(&mut buffer); + + // DefaultVisitor adds the message and variables to the buffer + let mut visitor = DefaultVisitor::new(writer, false); + event.record(&mut visitor); + + let msg = (event.metadata().level().into(), buffer); + + if let Some(logger) = JS_LOGGER.0.get() { + let _ = logger.call(Ok(msg.into()), ThreadsafeFunctionCallMode::NonBlocking); + }; + } + } + + #[napi] + pub fn init_napi_log(js_log_fn: ThreadsafeFunction>) { + let _ = JS_LOGGER.0.set(js_log_fn); + + // the log level hierarchy is determined by: + // - if RUST_LOG is detected at runtime + // - if RUST_LOG is provided at compile time + // - default to INFO + let filter = EnvFilter::builder() + .with_default_directive( + option_env!("RUST_LOG") + .unwrap_or("info") + .parse() + .expect("should provide valid log level at compile time."), + ) + // parse directives from the RUST_LOG environment variable, + // overriding the default directive for matching targets. + .from_env_lossy(); + + // With the `tracing-log` feature enabled for the `tracing_subscriber`, + // the registry below will initialize a log compatibility layer, which allows + // the subscriber to consume log::Records as though they were tracing Events. + // https://docs.rs/tracing-subscriber/latest/tracing_subscriber/util/trait.SubscriberInitExt.html#method.init + tracing_subscriber::registry() + .with(filter) + .with(JsLayer) + .init(); + } +} + +#[napi] +pub mod chromium_importer { + use std::collections::HashMap; + + use chromium_importer::{ + chromium::{ + DefaultInstalledBrowserRetriever, LoginImportResult as _LoginImportResult, + ProfileInfo as _ProfileInfo, + }, + metadata::NativeImporterMetadata as _NativeImporterMetadata, + }; + + #[napi(object)] + pub struct ProfileInfo { + pub id: String, + pub name: String, + } + + #[napi(object)] + pub struct Login { + pub url: String, + pub username: String, + pub password: String, + pub note: String, + } + + #[napi(object)] + pub struct LoginImportFailure { + pub url: String, + pub username: String, + pub error: String, + } + + #[napi(object)] + pub struct LoginImportResult { + pub login: Option, + pub failure: Option, + } + + #[napi(object)] + pub struct NativeImporterMetadata { + pub id: String, + pub loaders: Vec, + pub instructions: String, + } + + impl From<_LoginImportResult> for LoginImportResult { + fn from(l: _LoginImportResult) -> Self { + match l { + _LoginImportResult::Success(l) => LoginImportResult { + login: Some(Login { + url: l.url, + username: l.username, + password: l.password, + note: l.note, + }), + failure: None, + }, + _LoginImportResult::Failure(l) => LoginImportResult { + login: None, + failure: Some(LoginImportFailure { + url: l.url, + username: l.username, + error: l.error, + }), + }, + } + } + } + + impl From<_ProfileInfo> for ProfileInfo { + fn from(p: _ProfileInfo) -> Self { + ProfileInfo { + id: p.folder, + name: p.name, + } + } + } + + impl From<_NativeImporterMetadata> for NativeImporterMetadata { + fn from(m: _NativeImporterMetadata) -> Self { + NativeImporterMetadata { + id: m.id, + loaders: m.loaders, + instructions: m.instructions, + } + } + } + + #[napi] + /// Returns OS aware metadata describing supported Chromium based importers as a JSON string. + pub fn get_metadata() -> HashMap { + chromium_importer::metadata::get_supported_importers::() + .into_iter() + .map(|(browser, metadata)| (browser, NativeImporterMetadata::from(metadata))) + .collect() + } + + #[napi] + pub fn get_available_profiles(browser: String) -> napi::Result> { + chromium_importer::chromium::get_available_profiles(&browser) + .map(|profiles| profiles.into_iter().map(ProfileInfo::from).collect()) + .map_err(|e| napi::Error::from_reason(e.to_string())) + } + + #[napi] + pub async fn import_logins( + browser: String, + profile_id: String, + ) -> napi::Result> { + chromium_importer::chromium::import_logins(&browser, &profile_id) + .await + .map(|logins| logins.into_iter().map(LoginImportResult::from).collect()) + .map_err(|e| napi::Error::from_reason(e.to_string())) + } +} + +#[napi] +pub mod autotype { + #[napi] + pub fn get_foreground_window_title() -> napi::Result { + autotype::get_foreground_window_title().map_err(|_| { + napi::Error::from_reason( + "Autotype Error: failed to get foreground window title".to_string(), + ) + }) + } + + #[napi] + pub fn type_input( + input: Vec, + keyboard_shortcut: Vec, + ) -> napi::Result<(), napi::Status> { + autotype::type_input(&input, &keyboard_shortcut) + .map_err(|e| napi::Error::from_reason(format!("Autotype Error: {e}"))) + } +} diff --git a/apps/desktop/desktop_native/napi/src/logging.rs b/apps/desktop/desktop_native/napi/src/logging.rs deleted file mode 100644 index e5791065e4e..00000000000 --- a/apps/desktop/desktop_native/napi/src/logging.rs +++ /dev/null @@ -1,131 +0,0 @@ -#[napi] -pub mod logging { - //! `logging` is the interface between the native desktop's usage of the `tracing` crate - //! for logging, to intercept events and write to the JS space. - //! - //! # Example - //! - //! [Elec] 14:34:03.517 › [NAPI] [INFO] desktop_core::ssh_agent::platform_ssh_agent: Starting - //! SSH Agent server {socket=/Users/foo/.bitwarden-ssh-agent.sock} - - use std::{fmt::Write, sync::OnceLock}; - - use napi::{ - bindgen_prelude::FnArgs, - threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode}, - }; - use tracing::Level; - use tracing_subscriber::{ - filter::EnvFilter, - fmt::format::{DefaultVisitor, Writer}, - layer::SubscriberExt, - util::SubscriberInitExt, - Layer, - }; - - struct JsLogger(OnceLock>>); - static JS_LOGGER: JsLogger = JsLogger(OnceLock::new()); - - #[napi] - pub enum LogLevel { - Trace, - Debug, - Info, - Warn, - Error, - } - - impl From<&Level> for LogLevel { - fn from(level: &Level) -> Self { - match *level { - Level::TRACE => LogLevel::Trace, - Level::DEBUG => LogLevel::Debug, - Level::INFO => LogLevel::Info, - Level::WARN => LogLevel::Warn, - Level::ERROR => LogLevel::Error, - } - } - } - - // JsLayer lets us intercept events and write them to the JS Logger. - struct JsLayer; - - impl Layer for JsLayer - where - S: tracing::Subscriber, - { - // This function builds a log message buffer from the event data and - // calls the JS logger with it. - // - // For example, this log call: - // - // ``` - // mod supreme { - // mod module { - // let foo = "bar"; - // info!(best_variable_name = %foo, "Foo done it again."); - // } - // } - // ``` - // - // , results in the following string: - // - // [INFO] supreme::module: Foo done it again. {best_variable_name=bar} - fn on_event( - &self, - event: &tracing::Event<'_>, - _ctx: tracing_subscriber::layer::Context<'_, S>, - ) { - let mut buffer = String::new(); - - // create the preamble text that precedes the message and vars. e.g.: - // [INFO] desktop_core::ssh_agent::platform_ssh_agent: - let level = event.metadata().level().as_str(); - let module_path = event.metadata().module_path().unwrap_or_default(); - - write!(&mut buffer, "[{level}] {module_path}:") - .expect("Failed to write tracing event to buffer"); - - let writer = Writer::new(&mut buffer); - - // DefaultVisitor adds the message and variables to the buffer - let mut visitor = DefaultVisitor::new(writer, false); - event.record(&mut visitor); - - let msg = (event.metadata().level().into(), buffer); - - if let Some(logger) = JS_LOGGER.0.get() { - let _ = logger.call(Ok(msg.into()), ThreadsafeFunctionCallMode::NonBlocking); - }; - } - } - - #[napi] - pub fn init_napi_log(js_log_fn: ThreadsafeFunction>) { - let _ = JS_LOGGER.0.set(js_log_fn); - - // the log level hierarchy is determined by: - // - if RUST_LOG is detected at runtime - // - if RUST_LOG is provided at compile time - // - default to INFO - let filter = EnvFilter::builder() - .with_default_directive( - option_env!("RUST_LOG") - .unwrap_or("info") - .parse() - .expect("should provide valid log level at compile time."), - ) - // parse directives from the RUST_LOG environment variable, - // overriding the default directive for matching targets. - .from_env_lossy(); - - // With the `tracing-log` feature enabled for the `tracing_subscriber`, - // the registry below will initialize a log compatibility layer, which allows - // the subscriber to consume log::Records as though they were tracing Events. - // https://docs.rs/tracing-subscriber/latest/tracing_subscriber/util/trait.SubscriberInitExt.html#method.init - tracing_subscriber::registry() - .with(filter) - .with(JsLayer) - .init(); - } -} diff --git a/apps/desktop/desktop_native/napi/src/passkey_authenticator.rs b/apps/desktop/desktop_native/napi/src/passkey_authenticator.rs deleted file mode 100644 index 37796353b80..00000000000 --- a/apps/desktop/desktop_native/napi/src/passkey_authenticator.rs +++ /dev/null @@ -1,9 +0,0 @@ -#[napi] -pub mod passkey_authenticator { - #[napi] - pub fn register() -> napi::Result<()> { - crate::passkey_authenticator_internal::register().map_err(|e| { - napi::Error::from_reason(format!("Passkey registration failed - Error: {e} - {e:?}")) - }) - } -} diff --git a/apps/desktop/desktop_native/napi/src/passwords.rs b/apps/desktop/desktop_native/napi/src/passwords.rs deleted file mode 100644 index 763f338b0cb..00000000000 --- a/apps/desktop/desktop_native/napi/src/passwords.rs +++ /dev/null @@ -1,46 +0,0 @@ -#[napi] -pub mod passwords { - - /// The error message returned when a password is not found during retrieval or deletion. - #[napi] - pub const PASSWORD_NOT_FOUND: &str = desktop_core::password::PASSWORD_NOT_FOUND; - - /// Fetch the stored password from the keychain. - /// Throws {@link Error} with message {@link PASSWORD_NOT_FOUND} if the password does not exist. - #[napi] - pub async fn get_password(service: String, account: String) -> napi::Result { - desktop_core::password::get_password(&service, &account) - .await - .map_err(|e| napi::Error::from_reason(e.to_string())) - } - - /// Save the password to the keychain. Adds an entry if none exists otherwise updates the - /// existing entry. - #[napi] - pub async fn set_password( - service: String, - account: String, - password: String, - ) -> napi::Result<()> { - desktop_core::password::set_password(&service, &account, &password) - .await - .map_err(|e| napi::Error::from_reason(e.to_string())) - } - - /// Delete the stored password from the keychain. - /// Throws {@link Error} with message {@link PASSWORD_NOT_FOUND} if the password does not exist. - #[napi] - pub async fn delete_password(service: String, account: String) -> napi::Result<()> { - desktop_core::password::delete_password(&service, &account) - .await - .map_err(|e| napi::Error::from_reason(e.to_string())) - } - - /// Checks if the os secure storage is available - #[napi] - pub async fn is_available() -> napi::Result { - desktop_core::password::is_available() - .await - .map_err(|e| napi::Error::from_reason(e.to_string())) - } -} diff --git a/apps/desktop/desktop_native/napi/src/powermonitors.rs b/apps/desktop/desktop_native/napi/src/powermonitors.rs deleted file mode 100644 index eb673bdbe68..00000000000 --- a/apps/desktop/desktop_native/napi/src/powermonitors.rs +++ /dev/null @@ -1,26 +0,0 @@ -#[napi] -pub mod powermonitors { - use napi::{ - threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode}, - tokio, - }; - - #[napi] - pub async fn on_lock(callback: ThreadsafeFunction<()>) -> napi::Result<()> { - let (tx, mut rx) = tokio::sync::mpsc::channel::<()>(32); - desktop_core::powermonitor::on_lock(tx) - .await - .map_err(|e| napi::Error::from_reason(e.to_string()))?; - tokio::spawn(async move { - while let Some(()) = rx.recv().await { - callback.call(Ok(()), ThreadsafeFunctionCallMode::NonBlocking); - } - }); - Ok(()) - } - - #[napi] - pub async fn is_lock_monitor_available() -> napi::Result { - Ok(desktop_core::powermonitor::is_lock_monitor_available().await) - } -} diff --git a/apps/desktop/desktop_native/napi/src/processisolations.rs b/apps/desktop/desktop_native/napi/src/processisolations.rs deleted file mode 100644 index 6ab4a2a645d..00000000000 --- a/apps/desktop/desktop_native/napi/src/processisolations.rs +++ /dev/null @@ -1,23 +0,0 @@ -#[napi] -pub mod processisolations { - #[allow(clippy::unused_async)] // FIXME: Remove unused async! - #[napi] - pub async fn disable_coredumps() -> napi::Result<()> { - desktop_core::process_isolation::disable_coredumps() - .map_err(|e| napi::Error::from_reason(e.to_string())) - } - - #[allow(clippy::unused_async)] // FIXME: Remove unused async! - #[napi] - pub async fn is_core_dumping_disabled() -> napi::Result { - desktop_core::process_isolation::is_core_dumping_disabled() - .map_err(|e| napi::Error::from_reason(e.to_string())) - } - - #[allow(clippy::unused_async)] // FIXME: Remove unused async! - #[napi] - pub async fn isolate_process() -> napi::Result<()> { - desktop_core::process_isolation::isolate_process() - .map_err(|e| napi::Error::from_reason(e.to_string())) - } -} diff --git a/apps/desktop/desktop_native/napi/src/sshagent.rs b/apps/desktop/desktop_native/napi/src/sshagent.rs deleted file mode 100644 index 83eec090302..00000000000 --- a/apps/desktop/desktop_native/napi/src/sshagent.rs +++ /dev/null @@ -1,163 +0,0 @@ -#[napi] -pub mod sshagent { - use std::sync::Arc; - - use napi::{ - bindgen_prelude::Promise, - threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode}, - }; - use tokio::{self, sync::Mutex}; - use tracing::error; - - #[napi] - pub struct SshAgentState { - state: desktop_core::ssh_agent::BitwardenDesktopAgent, - } - - #[napi(object)] - pub struct PrivateKey { - pub private_key: String, - pub name: String, - pub cipher_id: String, - } - - #[napi(object)] - pub struct SshKey { - pub private_key: String, - pub public_key: String, - pub key_fingerprint: String, - } - - #[napi(object)] - pub struct SshUIRequest { - pub cipher_id: Option, - pub is_list: bool, - pub process_name: String, - pub is_forwarding: bool, - pub namespace: Option, - } - - #[allow(clippy::unused_async)] // FIXME: Remove unused async! - #[napi] - pub async fn serve( - callback: ThreadsafeFunction>, - ) -> napi::Result { - let (auth_request_tx, mut auth_request_rx) = - tokio::sync::mpsc::channel::(32); - let (auth_response_tx, auth_response_rx) = - tokio::sync::broadcast::channel::<(u32, bool)>(32); - let auth_response_tx_arc = Arc::new(Mutex::new(auth_response_tx)); - // Wrap callback in Arc so it can be shared across spawned tasks - let callback = Arc::new(callback); - tokio::spawn(async move { - let _ = auth_response_rx; - - while let Some(request) = auth_request_rx.recv().await { - let cloned_response_tx_arc = auth_response_tx_arc.clone(); - let cloned_callback = callback.clone(); - tokio::spawn(async move { - let auth_response_tx_arc = cloned_response_tx_arc; - let callback = cloned_callback; - // In NAPI v3, obtain the JS callback return as a Promise and await it - // in Rust - let (tx, rx) = std::sync::mpsc::channel::>(); - let status = callback.call_with_return_value( - Ok(SshUIRequest { - cipher_id: request.cipher_id, - is_list: request.is_list, - process_name: request.process_name, - is_forwarding: request.is_forwarding, - namespace: request.namespace, - }), - ThreadsafeFunctionCallMode::Blocking, - move |ret: Result, napi::Error>, _env| { - if let Ok(p) = ret { - let _ = tx.send(p); - } - Ok(()) - }, - ); - - let result = if status == napi::Status::Ok { - match rx.recv() { - Ok(promise) => match promise.await { - Ok(v) => v, - Err(e) => { - error!(error = %e, "UI callback promise rejected"); - false - } - }, - Err(e) => { - error!(error = %e, "Failed to receive UI callback promise"); - false - } - } - } else { - error!(error = ?status, "Calling UI callback failed"); - false - }; - - let _ = auth_response_tx_arc - .lock() - .await - .send((request.request_id, result)) - .expect("should be able to send auth response to agent"); - }); - } - }); - - match desktop_core::ssh_agent::BitwardenDesktopAgent::start_server( - auth_request_tx, - Arc::new(Mutex::new(auth_response_rx)), - ) { - Ok(state) => Ok(SshAgentState { state }), - Err(e) => Err(napi::Error::from_reason(e.to_string())), - } - } - - #[napi] - pub fn stop(agent_state: &mut SshAgentState) -> napi::Result<()> { - let bitwarden_agent_state = &mut agent_state.state; - bitwarden_agent_state.stop(); - Ok(()) - } - - #[napi] - pub fn is_running(agent_state: &mut SshAgentState) -> bool { - let bitwarden_agent_state = agent_state.state.clone(); - bitwarden_agent_state.is_running() - } - - #[napi] - pub fn set_keys( - agent_state: &mut SshAgentState, - new_keys: Vec, - ) -> napi::Result<()> { - let bitwarden_agent_state = &mut agent_state.state; - bitwarden_agent_state - .set_keys( - new_keys - .iter() - .map(|k| (k.private_key.clone(), k.name.clone(), k.cipher_id.clone())) - .collect(), - ) - .map_err(|e| napi::Error::from_reason(e.to_string()))?; - Ok(()) - } - - #[napi] - pub fn lock(agent_state: &mut SshAgentState) -> napi::Result<()> { - let bitwarden_agent_state = &mut agent_state.state; - bitwarden_agent_state - .lock() - .map_err(|e| napi::Error::from_reason(e.to_string())) - } - - #[napi] - pub fn clear_keys(agent_state: &mut SshAgentState) -> napi::Result<()> { - let bitwarden_agent_state = &mut agent_state.state; - bitwarden_agent_state - .clear_keys() - .map_err(|e| napi::Error::from_reason(e.to_string())) - } -} diff --git a/apps/desktop/desktop_native/napi/src/windows_registry.rs b/apps/desktop/desktop_native/napi/src/windows_registry.rs deleted file mode 100644 index e22e2ce46f5..00000000000 --- a/apps/desktop/desktop_native/napi/src/windows_registry.rs +++ /dev/null @@ -1,16 +0,0 @@ -#[napi] -pub mod windows_registry { - #[allow(clippy::unused_async)] // FIXME: Remove unused async! - #[napi] - pub async fn create_key(key: String, subkey: String, value: String) -> napi::Result<()> { - crate::registry::create_key(&key, &subkey, &value) - .map_err(|e| napi::Error::from_reason(e.to_string())) - } - - #[allow(clippy::unused_async)] // FIXME: Remove unused async! - #[napi] - pub async fn delete_key(key: String, subkey: String) -> napi::Result<()> { - crate::registry::delete_key(&key, &subkey) - .map_err(|e| napi::Error::from_reason(e.to_string())) - } -} From 8c6a5775a9612674df47bc9574e17b16cec9655d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 20 Feb 2026 14:40:15 -0600 Subject: [PATCH 59/89] [deps] SM: Update jest (#17554) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: cd-bitwarden <106776772+cd-bitwarden@users.noreply.github.com> --- package-lock.json | 16 ++++++++-------- package.json | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index 79541971b12..47ba7456b0a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -151,7 +151,7 @@ "jest-diff": "30.2.0", "jest-junit": "16.0.0", "jest-mock-extended": "3.0.7", - "jest-preset-angular": "14.6.1", + "jest-preset-angular": "14.6.2", "json5": "2.2.3", "lint-staged": "16.0.0", "mini-css-extract-plugin": "2.9.4", @@ -169,7 +169,7 @@ "storybook": "9.1.17", "style-loader": "4.0.0", "tailwindcss": "3.4.18", - "ts-jest": "29.4.5", + "ts-jest": "29.4.6", "ts-loader": "9.5.4", "tsconfig-paths-webpack-plugin": "4.2.0", "type-fest": "2.19.0", @@ -28537,9 +28537,9 @@ } }, "node_modules/jest-preset-angular": { - "version": "14.6.1", - "resolved": "https://registry.npmjs.org/jest-preset-angular/-/jest-preset-angular-14.6.1.tgz", - "integrity": "sha512-7q5x42wKrsF2ykOwGVzcXpr9p1X4FQJMU/DnH1tpvCmeOm5XqENdwD/xDZug+nP6G8SJPdioauwdsK/PMY/MpQ==", + "version": "14.6.2", + "resolved": "https://registry.npmjs.org/jest-preset-angular/-/jest-preset-angular-14.6.2.tgz", + "integrity": "sha512-QWnjfXrnYJX65D+iZXBrdQ0ABHSo6DGvcmL3dGYOdF+V2ZhDlqJwKTmt7nyiOcORPdCL+20P8y+Q1mmnjZTHKQ==", "dev": true, "license": "MIT", "dependencies": { @@ -41234,9 +41234,9 @@ "license": "Apache-2.0" }, "node_modules/ts-jest": { - "version": "29.4.5", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.5.tgz", - "integrity": "sha512-HO3GyiWn2qvTQA4kTgjDcXiMwYQt68a1Y8+JuLRVpdIzm+UOLSHgl/XqR4c6nzJkq5rOkjc02O2I7P7l/Yof0Q==", + "version": "29.4.6", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.6.tgz", + "integrity": "sha512-fSpWtOO/1AjSNQguk43hb/JCo16oJDnMJf3CdEGNkqsEX3t0KX96xvyX1D7PfLCpVoKu4MfVrqUkFyblYoY4lA==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 1795e93cf83..ecb49605114 100644 --- a/package.json +++ b/package.json @@ -118,7 +118,7 @@ "jest-diff": "30.2.0", "jest-junit": "16.0.0", "jest-mock-extended": "3.0.7", - "jest-preset-angular": "14.6.1", + "jest-preset-angular": "14.6.2", "json5": "2.2.3", "lint-staged": "16.0.0", "mini-css-extract-plugin": "2.9.4", @@ -136,7 +136,7 @@ "storybook": "9.1.17", "style-loader": "4.0.0", "tailwindcss": "3.4.18", - "ts-jest": "29.4.5", + "ts-jest": "29.4.6", "ts-loader": "9.5.4", "tsconfig-paths-webpack-plugin": "4.2.0", "type-fest": "2.19.0", From c01ce9f99d2e1751c6a61d262c662d8c236f14fb Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Fri, 20 Feb 2026 13:41:47 -0800 Subject: [PATCH 60/89] check for falsy orgnanizationId in cipher bulk collection assignment (#19088) --- apps/web/src/app/vault/individual-vault/vault.component.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/web/src/app/vault/individual-vault/vault.component.ts b/apps/web/src/app/vault/individual-vault/vault.component.ts index 1f80748ab29..a6b80291647 100644 --- a/apps/web/src/app/vault/individual-vault/vault.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault.component.ts @@ -1205,8 +1205,7 @@ export class VaultComponent implements OnInit, OnDestr let availableCollections: CollectionView[] = []; const orgId = - this.activeFilter.organizationId || - ciphers.find((c) => c.organizationId !== undefined)?.organizationId; + this.activeFilter.organizationId || ciphers.find((c) => !!c.organizationId)?.organizationId; if (orgId && orgId !== "MyVault") { const organization = this.allOrganizations.find((o) => o.id === orgId); From 60e97a4968b379d4916b5f085ea0237a10a28812 Mon Sep 17 00:00:00 2001 From: Jason Ng Date: Fri, 20 Feb 2026 16:47:44 -0500 Subject: [PATCH 61/89] [PM-32341] use an initial cache value in default cipher form (#19091) * update default cipher form cache with initial cache value --- .../services/default-cipher-form-cache.service.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/libs/vault/src/cipher-form/services/default-cipher-form-cache.service.ts b/libs/vault/src/cipher-form/services/default-cipher-form-cache.service.ts index d525dcd9afa..f83c2bbb15b 100644 --- a/libs/vault/src/cipher-form/services/default-cipher-form-cache.service.ts +++ b/libs/vault/src/cipher-form/services/default-cipher-form-cache.service.ts @@ -18,6 +18,12 @@ export class CipherFormCacheService { */ initializedWithValue: boolean; + /** + * The cipher form will overwrite the cache from various components when initialized + * To prevent this, we store the initial value of the cache when the service is initialized + */ + initialCacheValue: CipherView | null; + private cipherCache = this.viewCacheService.signal({ key: CIPHER_FORM_CACHE_KEY, initialValue: null, @@ -26,6 +32,7 @@ export class CipherFormCacheService { constructor() { this.initializedWithValue = !!this.cipherCache(); + this.initialCacheValue = this.cipherCache(); } /** @@ -42,13 +49,14 @@ export class CipherFormCacheService { * Returns the cached CipherView when available. */ getCachedCipherView(): CipherView | null { - return this.cipherCache(); + return this.initialCacheValue; } /** * Clear the cached CipherView. */ clearCache(): void { + this.initialCacheValue = null; this.cipherCache.set(null); } } From ef7df6b841ae87c069a6a45bf882e528fb5b2b26 Mon Sep 17 00:00:00 2001 From: Jackson Engstrom Date: Fri, 20 Feb 2026 14:28:54 -0800 Subject: [PATCH 62/89] [PM-30521] Add Autofill button to View Login screen for extension (#18766) * adds autofill button for cipher view * adds tests * changes autofill function for non login types * adds top margin to autofill button * adds more top margin to autofill button * only shows autofill button when autofill is allowed (not in a popout) * add button type * updates _domainMatched to take a tab param, updates how the component is passed through to slot * fixes tests from rename * adds comment about autofill tab checking behavior * removes diff markers --- .../item-more-options.component.ts | 2 + .../components/vault/view/view.component.html | 16 +- .../vault/view/view.component.spec.ts | 470 +++++++++++++++++- .../components/vault/view/view.component.ts | 127 ++++- libs/common/src/enums/feature-flag.enum.ts | 2 + .../cipher-view/cipher-view.component.html | 1 + .../item-details-v2.component.html | 1 + 7 files changed, 615 insertions(+), 4 deletions(-) diff --git a/apps/browser/src/vault/popup/components/vault/item-more-options/item-more-options.component.ts b/apps/browser/src/vault/popup/components/vault/item-more-options/item-more-options.component.ts index f7fe9ee1494..e564ca0ceea 100644 --- a/apps/browser/src/vault/popup/components/vault/item-more-options/item-more-options.component.ts +++ b/apps/browser/src/vault/popup/components/vault/item-more-options/item-more-options.component.ts @@ -218,6 +218,8 @@ export class ItemMoreOptionsComponent { return; } + //this tab checking should be moved into the vault-popup-autofill service in case the current tab is changed + //ticket: https://bitwarden.atlassian.net/browse/PM-32467 const currentTab = await firstValueFrom(this.vaultPopupAutofillService.currentAutofillTab$); if (!currentTab?.url) { diff --git a/apps/browser/src/vault/popup/components/vault/view/view.component.html b/apps/browser/src/vault/popup/components/vault/view/view.component.html index a3d65522022..0e07497cea9 100644 --- a/apps/browser/src/vault/popup/components/vault/view/view.component.html +++ b/apps/browser/src/vault/popup/components/vault/view/view.component.html @@ -11,7 +11,21 @@ @if (cipher) { - + + @if (showAutofillButton()) { + + } + } diff --git a/apps/browser/src/vault/popup/components/vault/view/view.component.spec.ts b/apps/browser/src/vault/popup/components/vault/view/view.component.spec.ts index 5c94af0205d..af31dee7550 100644 --- a/apps/browser/src/vault/popup/components/vault/view/view.component.spec.ts +++ b/apps/browser/src/vault/popup/components/vault/view/view.component.spec.ts @@ -2,7 +2,7 @@ import { ComponentFixture, fakeAsync, flush, TestBed, tick } from "@angular/core import { By } from "@angular/platform-browser"; import { ActivatedRoute, Router } from "@angular/router"; import { mock } from "jest-mock-extended"; -import { of, Subject } from "rxjs"; +import { BehaviorSubject, of, Subject } from "rxjs"; import { CollectionService } from "@bitwarden/admin-console/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; @@ -18,6 +18,7 @@ import { import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions"; 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 { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -33,6 +34,7 @@ import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folde import { CipherRepromptType, CipherType } from "@bitwarden/common/vault/enums"; import { CipherData } from "@bitwarden/common/vault/models/data/cipher.data"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view"; import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service"; import { TaskService } from "@bitwarden/common/vault/tasks"; import { DialogService, ToastService } from "@bitwarden/components"; @@ -47,6 +49,10 @@ import BrowserPopupUtils from "../../../../../platform/browser/browser-popup-uti import { PopupRouterCacheService } from "../../../../../platform/popup/view-cache/popup-router-cache.service"; import { VaultPopupAutofillService } from "../../../services/vault-popup-autofill.service"; import { VaultPopupScrollPositionService } from "../../../services/vault-popup-scroll-position.service"; +import { + AutofillConfirmationDialogComponent, + AutofillConfirmationDialogResult, +} from "../autofill-confirmation-dialog/autofill-confirmation-dialog.component"; import { ViewComponent } from "./view.component"; @@ -62,6 +68,7 @@ describe("ViewComponent", () => { const mockNavigate = jest.fn(); const collect = jest.fn().mockResolvedValue(null); const doAutofill = jest.fn().mockResolvedValue(true); + const doAutofillAndSave = jest.fn().mockResolvedValue(true); const copy = jest.fn().mockResolvedValue(true); const back = jest.fn().mockResolvedValue(null); const openSimpleDialog = jest.fn().mockResolvedValue(true); @@ -69,6 +76,8 @@ describe("ViewComponent", () => { const showToast = jest.fn(); const showPasswordPrompt = jest.fn().mockResolvedValue(true); const getFeatureFlag$ = jest.fn().mockReturnValue(of(true)); + const getFeatureFlag = jest.fn().mockResolvedValue(true); + const currentAutofillTab$ = of({ url: "https://example.com", id: 1 }); const mockCipher = { id: "122-333-444", @@ -87,8 +96,12 @@ describe("ViewComponent", () => { const mockPasswordRepromptService = { showPasswordPrompt, }; + const autofillAllowed$ = new BehaviorSubject(true); const mockVaultPopupAutofillService = { doAutofill, + doAutofillAndSave, + currentAutofillTab$, + autofillAllowed$, }; const mockCopyCipherFieldService = { copy, @@ -112,12 +125,15 @@ describe("ViewComponent", () => { mockNavigate.mockClear(); collect.mockClear(); doAutofill.mockClear(); + doAutofillAndSave.mockClear(); copy.mockClear(); stop.mockClear(); openSimpleDialog.mockClear(); back.mockClear(); showToast.mockClear(); showPasswordPrompt.mockClear(); + getFeatureFlag.mockClear(); + autofillAllowed$.next(true); cipherArchiveService.hasArchiveFlagEnabled$ = of(true); cipherArchiveService.userCanArchive$.mockReturnValue(of(false)); cipherArchiveService.archiveWithServer.mockResolvedValue({ id: "122-333-444" } as CipherData); @@ -137,7 +153,7 @@ describe("ViewComponent", () => { { provide: VaultPopupScrollPositionService, useValue: { stop } }, { provide: VaultPopupAutofillService, useValue: mockVaultPopupAutofillService }, { provide: ToastService, useValue: { showToast } }, - { provide: ConfigService, useValue: { getFeatureFlag$ } }, + { provide: ConfigService, useValue: { getFeatureFlag$, getFeatureFlag } }, { provide: I18nService, useValue: { @@ -203,6 +219,8 @@ describe("ViewComponent", () => { provide: DomainSettingsService, useValue: { showFavicons$: of(true), + resolvedDefaultUriMatchStrategy$: of(UriMatchStrategy.Domain), + getUrlEquivalentDomains: jest.fn().mockReturnValue(of([])), }, }, { @@ -697,4 +715,452 @@ describe("ViewComponent", () => { expect(badge).toBeFalsy(); }); }); + + describe("showAutofillButton", () => { + beforeEach(() => { + component.cipher = { ...mockCipher, type: CipherType.Login } as CipherView; + }); + + it("returns true when feature flag is enabled, cipher is a login, and not archived/deleted", fakeAsync(() => { + getFeatureFlag$.mockReturnValue(of(true)); + autofillAllowed$.next(true); + + // Recreate component to pick up the signal values + fixture = TestBed.createComponent(ViewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + + component.cipher = { + ...mockCipher, + type: CipherType.Login, + isArchived: false, + isDeleted: false, + } as CipherView; + + flush(); + + const result = component.showAutofillButton(); + + expect(result).toBe(true); + })); + + it("returns true for Card type when conditions are met", fakeAsync(() => { + getFeatureFlag$.mockReturnValue(of(true)); + autofillAllowed$.next(true); + + // Recreate component to pick up the signal values + fixture = TestBed.createComponent(ViewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + + component.cipher = { + ...mockCipher, + type: CipherType.Card, + isArchived: false, + isDeleted: false, + } as CipherView; + + flush(); + + const result = component.showAutofillButton(); + + expect(result).toBe(true); + })); + + it("returns true for Identity type when conditions are met", fakeAsync(() => { + getFeatureFlag$.mockReturnValue(of(true)); + autofillAllowed$.next(true); + + // Recreate component to pick up the signal values + fixture = TestBed.createComponent(ViewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + + component.cipher = { + ...mockCipher, + type: CipherType.Identity, + isArchived: false, + isDeleted: false, + } as CipherView; + + flush(); + + const result = component.showAutofillButton(); + + expect(result).toBe(true); + })); + + it("returns false when feature flag is disabled", fakeAsync(() => { + getFeatureFlag$.mockReturnValue(of(false)); + + // Recreate component to pick up the new feature flag value + fixture = TestBed.createComponent(ViewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + + component.cipher = { + ...mockCipher, + type: CipherType.Login, + isArchived: false, + isDeleted: false, + } as CipherView; + + flush(); + + const result = component.showAutofillButton(); + + expect(result).toBe(false); + })); + + it("returns false when autofill is not allowed", fakeAsync(() => { + getFeatureFlag$.mockReturnValue(of(true)); + autofillAllowed$.next(false); + + // Recreate component to pick up the new autofillAllowed value + fixture = TestBed.createComponent(ViewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + + component.cipher = { + ...mockCipher, + type: CipherType.Login, + isArchived: false, + isDeleted: false, + } as CipherView; + + flush(); + + const result = component.showAutofillButton(); + + expect(result).toBe(false); + })); + + it("returns false for SecureNote type", fakeAsync(() => { + getFeatureFlag$.mockReturnValue(of(true)); + autofillAllowed$.next(true); + + // Recreate component to pick up the signal values + fixture = TestBed.createComponent(ViewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + + component.cipher = { + ...mockCipher, + type: CipherType.SecureNote, + isArchived: false, + isDeleted: false, + } as CipherView; + + flush(); + + const result = component.showAutofillButton(); + + expect(result).toBe(false); + })); + + it("returns false for SshKey type", fakeAsync(() => { + getFeatureFlag$.mockReturnValue(of(true)); + autofillAllowed$.next(true); + + // Recreate component to pick up the signal values + fixture = TestBed.createComponent(ViewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + + component.cipher = { + ...mockCipher, + type: CipherType.SshKey, + isArchived: false, + isDeleted: false, + } as CipherView; + + flush(); + + const result = component.showAutofillButton(); + + expect(result).toBe(false); + })); + + it("returns false when cipher is archived", fakeAsync(() => { + getFeatureFlag$.mockReturnValue(of(true)); + autofillAllowed$.next(true); + + // Recreate component to pick up the signal values + fixture = TestBed.createComponent(ViewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + + component.cipher = { + ...mockCipher, + type: CipherType.Login, + isArchived: true, + isDeleted: false, + } as CipherView; + + flush(); + + const result = component.showAutofillButton(); + + expect(result).toBe(false); + })); + + it("returns false when cipher is deleted", fakeAsync(() => { + getFeatureFlag$.mockReturnValue(of(true)); + autofillAllowed$.next(true); + + // Recreate component to pick up the signal values + fixture = TestBed.createComponent(ViewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + + component.cipher = { + ...mockCipher, + type: CipherType.Login, + isArchived: false, + isDeleted: true, + } as CipherView; + + flush(); + + const result = component.showAutofillButton(); + + expect(result).toBe(false); + })); + }); + + describe("doAutofill", () => { + let dialogService: DialogService; + const originalCurrentAutofillTab$ = currentAutofillTab$; + + beforeEach(() => { + dialogService = TestBed.inject(DialogService); + + component.cipher = { + ...mockCipher, + type: CipherType.Login, + login: { + username: "test", + password: "test", + uris: [ + { + uri: "https://example.com", + match: null, + } as LoginUriView, + ], + }, + edit: true, + } as CipherView; + }); + + afterEach(() => { + // Restore original observable to prevent test pollution + mockVaultPopupAutofillService.currentAutofillTab$ = originalCurrentAutofillTab$; + }); + + it("returns early when feature flag is disabled", async () => { + getFeatureFlag.mockResolvedValue(false); + + await component.doAutofill(); + + expect(doAutofill).not.toHaveBeenCalled(); + expect(openSimpleDialog).not.toHaveBeenCalled(); + }); + + it("shows exact match dialog when no URIs and default strategy is Exact", async () => { + getFeatureFlag.mockResolvedValue(true); + component.cipher.login.uris = []; + (component as any).uriMatchStrategy$ = of(UriMatchStrategy.Exact); + + await component.doAutofill(); + + expect(openSimpleDialog).toHaveBeenCalledWith({ + title: { key: "cannotAutofill" }, + content: { key: "cannotAutofillExactMatch" }, + type: "info", + acceptButtonText: { key: "okay" }, + cancelButtonText: null, + }); + expect(doAutofill).not.toHaveBeenCalled(); + }); + + it("shows exact match dialog when all URIs have exact match strategy", async () => { + getFeatureFlag.mockResolvedValue(true); + component.cipher.login.uris = [ + { uri: "https://example.com", match: UriMatchStrategy.Exact } as LoginUriView, + { uri: "https://example2.com", match: UriMatchStrategy.Exact } as LoginUriView, + ]; + + await component.doAutofill(); + + expect(openSimpleDialog).toHaveBeenCalledWith({ + title: { key: "cannotAutofill" }, + content: { key: "cannotAutofillExactMatch" }, + type: "info", + acceptButtonText: { key: "okay" }, + cancelButtonText: null, + }); + expect(doAutofill).not.toHaveBeenCalled(); + }); + + it("shows error dialog when current tab URL is unavailable", async () => { + getFeatureFlag.mockResolvedValue(true); + mockVaultPopupAutofillService.currentAutofillTab$ = of({ url: null, id: 1 }); + + await component.doAutofill(); + + expect(openSimpleDialog).toHaveBeenCalledWith({ + title: { key: "error" }, + content: { key: "errorGettingAutoFillData" }, + type: "danger", + }); + expect(doAutofill).not.toHaveBeenCalled(); + }); + + it("autofills directly when domain matches", async () => { + getFeatureFlag.mockResolvedValue(true); + jest.spyOn(component as any, "_domainMatched").mockResolvedValue(true); + + await component.doAutofill(); + + expect(doAutofill).toHaveBeenCalledWith(component.cipher, true, true); + }); + + it("shows confirmation dialog when domain does not match", async () => { + getFeatureFlag.mockResolvedValue(true); + jest.spyOn(component as any, "_domainMatched").mockResolvedValue(false); + + const mockDialogRef = { + closed: of(AutofillConfirmationDialogResult.Canceled), + }; + jest.spyOn(AutofillConfirmationDialogComponent, "open").mockReturnValue(mockDialogRef as any); + + await component.doAutofill(); + + expect(AutofillConfirmationDialogComponent.open).toHaveBeenCalledWith(dialogService, { + data: { + currentUrl: "https://example.com", + savedUrls: ["https://example.com"], + viewOnly: false, + }, + }); + }); + + it("does not autofill when user cancels confirmation dialog", async () => { + getFeatureFlag.mockResolvedValue(true); + jest.spyOn(component as any, "_domainMatched").mockResolvedValue(false); + + const mockDialogRef = { + closed: of(AutofillConfirmationDialogResult.Canceled), + }; + jest.spyOn(AutofillConfirmationDialogComponent, "open").mockReturnValue(mockDialogRef as any); + + await component.doAutofill(); + + expect(doAutofill).not.toHaveBeenCalled(); + expect(doAutofillAndSave).not.toHaveBeenCalled(); + }); + + it("autofills only when user selects AutofilledOnly", async () => { + getFeatureFlag.mockResolvedValue(true); + jest.spyOn(component as any, "_domainMatched").mockResolvedValue(false); + + const mockDialogRef = { + closed: of(AutofillConfirmationDialogResult.AutofilledOnly), + }; + jest.spyOn(AutofillConfirmationDialogComponent, "open").mockReturnValue(mockDialogRef as any); + + await component.doAutofill(); + + expect(doAutofill).toHaveBeenCalledWith(component.cipher, true, true); + expect(doAutofillAndSave).not.toHaveBeenCalled(); + }); + + it("autofills and saves URL when user selects AutofillAndUrlAdded", async () => { + getFeatureFlag.mockResolvedValue(true); + jest.spyOn(component as any, "_domainMatched").mockResolvedValue(false); + + const mockDialogRef = { + closed: of(AutofillConfirmationDialogResult.AutofillAndUrlAdded), + }; + jest.spyOn(AutofillConfirmationDialogComponent, "open").mockReturnValue(mockDialogRef as any); + + await component.doAutofill(); + + expect(doAutofillAndSave).toHaveBeenCalledWith(component.cipher, true, true); + expect(doAutofill).not.toHaveBeenCalled(); + }); + + it("passes viewOnly as true when cipher is not editable", async () => { + getFeatureFlag.mockResolvedValue(true); + jest.spyOn(component as any, "_domainMatched").mockResolvedValue(false); + component.cipher.edit = false; + + const mockDialogRef = { + closed: of(AutofillConfirmationDialogResult.Canceled), + }; + const openSpy = jest + .spyOn(AutofillConfirmationDialogComponent, "open") + .mockReturnValue(mockDialogRef as any); + + await component.doAutofill(); + + expect(openSpy).toHaveBeenCalledWith(dialogService, { + data: { + currentUrl: "https://example.com", + savedUrls: ["https://example.com"], + viewOnly: true, + }, + }); + }); + + it("filters out URIs without uri property", async () => { + getFeatureFlag.mockResolvedValue(true); + jest.spyOn(component as any, "_domainMatched").mockResolvedValue(false); + component.cipher.login.uris = [ + { uri: "https://example.com" } as LoginUriView, + { uri: null } as LoginUriView, + { uri: "https://example2.com" } as LoginUriView, + ]; + + const mockDialogRef = { + closed: of(AutofillConfirmationDialogResult.Canceled), + }; + const openSpy = jest + .spyOn(AutofillConfirmationDialogComponent, "open") + .mockReturnValue(mockDialogRef as any); + + await component.doAutofill(); + + expect(openSpy).toHaveBeenCalledWith(dialogService, { + data: { + currentUrl: "https://example.com", + savedUrls: ["https://example.com", "https://example2.com"], + viewOnly: false, + }, + }); + }); + + it("handles cipher with no login uris gracefully", async () => { + getFeatureFlag.mockResolvedValue(true); + jest.spyOn(component as any, "_domainMatched").mockResolvedValue(false); + component.cipher.login.uris = null; + + const mockDialogRef = { + closed: of(AutofillConfirmationDialogResult.Canceled), + }; + const openSpy = jest + .spyOn(AutofillConfirmationDialogComponent, "open") + .mockReturnValue(mockDialogRef as any); + + await component.doAutofill(); + + expect(openSpy).toHaveBeenCalledWith(dialogService, { + data: { + currentUrl: "https://example.com", + savedUrls: [], + viewOnly: false, + }, + }); + }); + }); }); diff --git a/apps/browser/src/vault/popup/components/vault/view/view.component.ts b/apps/browser/src/vault/popup/components/vault/view/view.component.ts index 48402a957d6..5166dbcf8db 100644 --- a/apps/browser/src/vault/popup/components/vault/view/view.component.ts +++ b/apps/browser/src/vault/popup/components/vault/view/view.component.ts @@ -2,7 +2,7 @@ // @ts-strict-ignore import { CommonModule } from "@angular/common"; import { Component } from "@angular/core"; -import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { takeUntilDestroyed, toSignal } from "@angular/core/rxjs-interop"; import { FormsModule } from "@angular/forms"; import { ActivatedRoute, Router } from "@angular/router"; import { firstValueFrom, Observable, switchMap, of, map } from "rxjs"; @@ -21,7 +21,11 @@ import { SHOW_AUTOFILL_BUTTON, UPDATE_PASSWORD, } from "@bitwarden/common/autofill/constants"; +import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; import { EventType } from "@bitwarden/common/enums"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { UriMatchStrategy } from "@bitwarden/common/models/domain/domain-service"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { UserId } from "@bitwarden/common/types/guid"; @@ -32,6 +36,7 @@ import { ViewPasswordHistoryService } from "@bitwarden/common/vault/abstractions import { CipherRepromptType, CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service"; +import { CipherViewLikeUtils } from "@bitwarden/common/vault/utils/cipher-view-like-utils"; import { filterOutNullish } from "@bitwarden/common/vault/utils/observable-utilities"; import { AsyncActionsModule, @@ -66,6 +71,10 @@ import { VaultPopupAutofillService } from "../../../services/vault-popup-autofil import { VaultPopupScrollPositionService } from "../../../services/vault-popup-scroll-position.service"; import { closeViewVaultItemPopout, VaultPopoutType } from "../../../utils/vault-popout-window"; import { ROUTES_AFTER_EDIT_DELETION } from "../add-edit/add-edit.component"; +import { + AutofillConfirmationDialogComponent, + AutofillConfirmationDialogResult, +} from "../autofill-confirmation-dialog/autofill-confirmation-dialog.component"; /** * The types of actions that can be triggered when loading the view vault item popout via the @@ -118,6 +127,13 @@ export class ViewComponent { senderTabId?: number; routeAfterDeletion?: ROUTES_AFTER_EDIT_DELETION; + //feature flag + private readonly pm30521FeatureFlag = toSignal( + this.configService.getFeatureFlag$(FeatureFlag.PM30521_AutofillButtonViewLoginScreen), + ); + + private readonly autofillAllowed = toSignal(this.vaultPopupAutofillService.autofillAllowed$); + private uriMatchStrategy$ = this.domainSettingsService.resolvedDefaultUriMatchStrategy$; protected showFooter$: Observable; protected userCanArchive$ = this.accountService.activeAccount$ .pipe(getUserId) @@ -142,6 +158,8 @@ export class ViewComponent { private popupScrollPositionService: VaultPopupScrollPositionService, private archiveService: CipherArchiveService, private archiveCipherUtilsService: ArchiveCipherUtilitiesService, + private domainSettingsService: DomainSettingsService, + private configService: ConfigService, ) { this.subscribeToParams(); } @@ -322,6 +340,113 @@ export class ViewComponent { : this.cipherService.softDeleteWithServer(this.cipher.id, this.activeUserId); } + showAutofillButton(): boolean { + //feature flag + if (!this.pm30521FeatureFlag()) { + return false; + } + + if (!this.autofillAllowed()) { + return false; + } + + const validAutofillType = ( + [CipherType.Login, CipherType.Card, CipherType.Identity] as CipherType[] + ).includes(CipherViewLikeUtils.getType(this.cipher)); + + return validAutofillType && !(this.cipher.isArchived || this.cipher.isDeleted); + } + + async doAutofill() { + //feature flag + if ( + !(await this.configService.getFeatureFlag(FeatureFlag.PM30521_AutofillButtonViewLoginScreen)) + ) { + return; + } + + //for non login types that are still auto-fillable + if (CipherViewLikeUtils.getType(this.cipher) !== CipherType.Login) { + await this.vaultPopupAutofillService.doAutofill(this.cipher, true, true); + return; + } + + const uris = this.cipher.login?.uris ?? []; + const uriMatchStrategy = await firstValueFrom(this.uriMatchStrategy$); + + const showExactMatchDialog = + uris.length === 0 + ? uriMatchStrategy === UriMatchStrategy.Exact + : // all saved URIs are exact match + uris.every((u) => (u.match ?? uriMatchStrategy) === UriMatchStrategy.Exact); + + if (showExactMatchDialog) { + await this.dialogService.openSimpleDialog({ + title: { key: "cannotAutofill" }, + content: { key: "cannotAutofillExactMatch" }, + type: "info", + acceptButtonText: { key: "okay" }, + cancelButtonText: null, + }); + return; + } + + //this tab checking should be moved into the vault-popup-autofill service in case the current tab is changed + //ticket: https://bitwarden.atlassian.net/browse/PM-32467 + const currentTab = await firstValueFrom(this.vaultPopupAutofillService.currentAutofillTab$); + + if (!currentTab?.url) { + await this.dialogService.openSimpleDialog({ + title: { key: "error" }, + content: { key: "errorGettingAutoFillData" }, + type: "danger", + }); + return; + } + + if (await this._domainMatched(currentTab)) { + await this.vaultPopupAutofillService.doAutofill(this.cipher, true, true); + return; + } + + const ref = AutofillConfirmationDialogComponent.open(this.dialogService, { + data: { + currentUrl: currentTab?.url || "", + savedUrls: this.cipher.login?.uris?.filter((u) => u.uri).map((u) => u.uri!) ?? [], + viewOnly: !this.cipher.edit, + }, + }); + + const result = await firstValueFrom(ref.closed); + + switch (result) { + case AutofillConfirmationDialogResult.Canceled: + return; + case AutofillConfirmationDialogResult.AutofilledOnly: + await this.vaultPopupAutofillService.doAutofill(this.cipher, true, true); + return; + case AutofillConfirmationDialogResult.AutofillAndUrlAdded: + await this.vaultPopupAutofillService.doAutofillAndSave(this.cipher, true, true); + return; + } + } + + private async _domainMatched(currentTab: chrome.tabs.Tab): Promise { + const equivalentDomains = await firstValueFrom( + this.domainSettingsService.getUrlEquivalentDomains(currentTab?.url), + ); + const defaultMatch = await firstValueFrom( + this.domainSettingsService.resolvedDefaultUriMatchStrategy$, + ); + + return CipherViewLikeUtils.matchesUri( + this.cipher, + currentTab?.url, + equivalentDomains, + defaultMatch, + ); + } + /** * Handles the load action for the view vault item popout. These actions are typically triggered * via the extension context menu. It is necessary to render the view for items that have password diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index 0cd97eb7f2e..c9e2fa17dd6 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -70,6 +70,7 @@ export enum FeatureFlag { BrowserPremiumSpotlight = "pm-23384-browser-premium-spotlight", MigrateMyVaultToMyItems = "pm-20558-migrate-myvault-to-myitems", PM27632_SdkCipherCrudOperations = "pm-27632-cipher-crud-operations-to-sdk", + PM30521_AutofillButtonViewLoginScreen = "pm-30521-autofill-button-view-login-screen", PM29438_WelcomeDialogWithExtensionPrompt = "pm-29438-welcome-dialog-with-extension-prompt", PM29438_DialogWithExtensionPromptAccountAge = "pm-29438-dialog-with-extension-prompt-account-age", PM29437_WelcomeDialog = "pm-29437-welcome-dialog-no-ext-prompt", @@ -139,6 +140,7 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.BrowserPremiumSpotlight]: FALSE, [FeatureFlag.PM27632_SdkCipherCrudOperations]: FALSE, [FeatureFlag.MigrateMyVaultToMyItems]: FALSE, + [FeatureFlag.PM30521_AutofillButtonViewLoginScreen]: FALSE, [FeatureFlag.PM29438_WelcomeDialogWithExtensionPrompt]: FALSE, [FeatureFlag.PM29438_DialogWithExtensionPromptAccountAge]: 5, [FeatureFlag.PM29437_WelcomeDialog]: FALSE, diff --git a/libs/vault/src/cipher-view/cipher-view.component.html b/libs/vault/src/cipher-view/cipher-view.component.html index 05d2ecede72..813d1452225 100644 --- a/libs/vault/src/cipher-view/cipher-view.component.html +++ b/libs/vault/src/cipher-view/cipher-view.component.html @@ -37,6 +37,7 @@ [folder]="folder()" [hideOwner]="isAdminConsole()" > + 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 edf17f0921c..5687da0a212 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 @@ -89,5 +89,6 @@ }
+ From 2e284c5e5ad02a70035a9f44269fa92d7187279b Mon Sep 17 00:00:00 2001 From: Sola Date: Mon, 23 Feb 2026 16:50:13 +0800 Subject: [PATCH 63/89] Fix biometric authentication in sandboxed environments (Flatpak, Snap, etc.) (#18625) Biometric authentication was failing in Flatpak with the error "Unix process subject does not have uid set". This occurred because polkit could not validate the sandboxed PID against the host PID namespace. Use polkit's system-bus-name subject type instead of unix-process. This allows polkit to query D-Bus for the connection owner's host PID and credentials, bypassing the PID namespace issue. Includes fallback to unix-process for edge cases where D-Bus unique name is unavailable. --- .../desktop_native/core/src/biometric/unix.rs | 27 ++++++++++++++++++- .../core/src/biometric_v2/linux.rs | 27 ++++++++++++++++++- 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/apps/desktop/desktop_native/core/src/biometric/unix.rs b/apps/desktop/desktop_native/core/src/biometric/unix.rs index 3f4f10a1fcf..f120408a9e5 100644 --- a/apps/desktop/desktop_native/core/src/biometric/unix.rs +++ b/apps/desktop/desktop_native/core/src/biometric/unix.rs @@ -21,7 +21,32 @@ impl super::BiometricTrait for Biometric { async fn prompt(_hwnd: Vec, _message: String) -> Result { let connection = Connection::system().await?; let proxy = AuthorityProxy::new(&connection).await?; - let subject = Subject::new_for_owner(std::process::id(), None, None)?; + + // Use system-bus-name instead of unix-process to avoid PID namespace issues in + // sandboxed environments (e.g., Flatpak). When using unix-process with a PID from + // inside the sandbox, polkit cannot validate it against the host PID namespace. + // + // By using system-bus-name, polkit queries D-Bus for the connection's credentials, + // which includes the correct host PID and UID, avoiding namespace mismatches. + // + // If D-Bus unique name is not available, fall back to the traditional unix-process + // approach for compatibility with non-sandboxed environments. + let subject = if let Some(bus_name) = connection.unique_name() { + use zbus::zvariant::{OwnedValue, Str}; + let mut subject_details = std::collections::HashMap::new(); + subject_details.insert( + "name".to_string(), + OwnedValue::from(Str::from(bus_name.as_str())), + ); + Subject { + subject_kind: "system-bus-name".to_string(), + subject_details, + } + } else { + // Fallback: use unix-process with PID (may not work in sandboxed environments) + Subject::new_for_owner(std::process::id(), None, None)? + }; + let details = std::collections::HashMap::new(); let result = proxy .check_authorization( diff --git a/apps/desktop/desktop_native/core/src/biometric_v2/linux.rs b/apps/desktop/desktop_native/core/src/biometric_v2/linux.rs index ef6527e7b26..2656bd3fdf9 100644 --- a/apps/desktop/desktop_native/core/src/biometric_v2/linux.rs +++ b/apps/desktop/desktop_native/core/src/biometric_v2/linux.rs @@ -96,7 +96,32 @@ async fn polkit_authenticate_bitwarden_policy() -> Result { let connection = Connection::system().await?; let proxy = AuthorityProxy::new(&connection).await?; - let subject = Subject::new_for_owner(std::process::id(), None, None)?; + + // Use system-bus-name instead of unix-process to avoid PID namespace issues in + // sandboxed environments (e.g., Flatpak). When using unix-process with a PID from + // inside the sandbox, polkit cannot validate it against the host PID namespace. + // + // By using system-bus-name, polkit queries D-Bus for the connection's credentials, + // which includes the correct host PID and UID, avoiding namespace mismatches. + // + // If D-Bus unique name is not available, fall back to the traditional unix-process + // approach for compatibility with non-sandboxed environments. + let subject = if let Some(bus_name) = connection.unique_name() { + use zbus::zvariant::{OwnedValue, Str}; + let mut subject_details = std::collections::HashMap::new(); + subject_details.insert( + "name".to_string(), + OwnedValue::from(Str::from(bus_name.as_str())), + ); + Subject { + subject_kind: "system-bus-name".to_string(), + subject_details, + } + } else { + // Fallback: use unix-process with PID (may not work in sandboxed environments) + Subject::new_for_owner(std::process::id(), None, None)? + }; + let details = std::collections::HashMap::new(); let authorization_result = proxy .check_authorization( From 760b426c22d575631a8b6c0908bfc683093e9bc5 Mon Sep 17 00:00:00 2001 From: "bw-ghapp[bot]" <178206702+bw-ghapp[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 04:34:43 -0600 Subject: [PATCH 64/89] Autosync the updated translations (#19129) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/desktop/src/locales/cs/messages.json | 4 +-- apps/desktop/src/locales/de/messages.json | 6 ++-- apps/desktop/src/locales/it/messages.json | 30 ++++++++++---------- apps/desktop/src/locales/zh_CN/messages.json | 6 ++-- 4 files changed, 23 insertions(+), 23 deletions(-) diff --git a/apps/desktop/src/locales/cs/messages.json b/apps/desktop/src/locales/cs/messages.json index 0772645a8d4..e5d009cbe2c 100644 --- a/apps/desktop/src/locales/cs/messages.json +++ b/apps/desktop/src/locales/cs/messages.json @@ -1024,7 +1024,7 @@ "message": "Neplatný ověřovací kód" }, "invalidEmailOrVerificationCode": { - "message": "Invalid email or verification code" + "message": "Neplatný e-mail nebo ověřovací kód" }, "continue": { "message": "Pokračovat" @@ -1058,7 +1058,7 @@ "message": "Ověřovací aplikace" }, "authenticatorAppDescV2": { - "message": "Zadejte kód vygenerovaný ověřovací aplikací, jako je Autentikátor Bitwarden.", + "message": "Zadejte kód vygenerovaný ověřovací aplikací, jako je Bitwarden Authenticator.", "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, "yubiKeyTitleV2": { diff --git a/apps/desktop/src/locales/de/messages.json b/apps/desktop/src/locales/de/messages.json index fb045e30489..6d7f8843a32 100644 --- a/apps/desktop/src/locales/de/messages.json +++ b/apps/desktop/src/locales/de/messages.json @@ -1024,7 +1024,7 @@ "message": "Ungültiger Verifizierungscode" }, "invalidEmailOrVerificationCode": { - "message": "Invalid email or verification code" + "message": "E-Mail oder Verifizierungscode ungültig" }, "continue": { "message": "Weiter" @@ -4619,12 +4619,12 @@ "message": "Gib mehrere E-Mail-Adressen ein, indem du sie mit einem Komma trennst." }, "emailsRequiredChangeAccessType": { - "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + "message": "E-Mail-Verifizierung erfordert mindestens eine E-Mail-Adresse. Ändere den Zugriffstyp oben, um alle E-Mails zu entfernen." }, "emailPlaceholder": { "message": "benutzer@bitwarden.com, benutzer@acme.com" }, "userVerificationFailed": { - "message": "User verification failed." + "message": "Benutzerverifizierung fehlgeschlagen." } } diff --git a/apps/desktop/src/locales/it/messages.json b/apps/desktop/src/locales/it/messages.json index c394dd84a6f..ba1bb9e5346 100644 --- a/apps/desktop/src/locales/it/messages.json +++ b/apps/desktop/src/locales/it/messages.json @@ -1024,7 +1024,7 @@ "message": "Codice di verifica non valido" }, "invalidEmailOrVerificationCode": { - "message": "Invalid email or verification code" + "message": "Codice di verifica non valido" }, "continue": { "message": "Continua" @@ -4391,10 +4391,10 @@ "message": "Gli elementi archiviati appariranno qui e saranno esclusi dai risultati di ricerca e dal riempimento automatico." }, "itemArchiveToast": { - "message": "Item archived" + "message": "Elemento archiviato" }, "itemUnarchivedToast": { - "message": "Item unarchived" + "message": "Elemento estratto dall'archivio" }, "archiveItem": { "message": "Archivia elemento" @@ -4490,7 +4490,7 @@ "message": "Azione al timeout" }, "errorCannotDecrypt": { - "message": "Error: Cannot decrypt" + "message": "Errore: impossibile decrittare" }, "sessionTimeoutHeader": { "message": "Timeout della sessione" @@ -4591,40 +4591,40 @@ "message": "Perché vedo questo avviso?" }, "sendPasswordHelperText": { - "message": "Individuals will need to enter the password to view this Send", + "message": "I destinatari dovranno inserire la password per visualizzare questo Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "emailProtected": { - "message": "Email protected" + "message": "Email protetta" }, "emails": { - "message": "Emails" + "message": "Indirizzi email" }, "noAuth": { - "message": "Anyone with the link" + "message": "Chiunque abbia il link" }, "anyOneWithPassword": { - "message": "Anyone with a password set by you" + "message": "Chiunque abbia una password impostata da te" }, "whoCanView": { - "message": "Who can view" + "message": "Chi può visualizzare" }, "specificPeople": { - "message": "Specific people" + "message": "Persone specifiche" }, "emailVerificationDesc": { - "message": "After sharing this Send link, individuals will need to verify their email with a code to view this Send." + "message": "I destinatari dovranno verificare il loro indirizzo email con un codice per poter visualizzare il Send." }, "enterMultipleEmailsSeparatedByComma": { - "message": "Enter multiple emails by separating with a comma." + "message": "Inserisci più indirizzi email separandoli con virgole." }, "emailsRequiredChangeAccessType": { - "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + "message": "La verifica via email richiede almeno un indirizzo email. Per rimuovere tutte le email, modifica il tipo di accesso qui sopra." }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, "userVerificationFailed": { - "message": "User verification failed." + "message": "Verifica dell'utente non riuscita." } } diff --git a/apps/desktop/src/locales/zh_CN/messages.json b/apps/desktop/src/locales/zh_CN/messages.json index ad09c8f032e..bd3a897ffab 100644 --- a/apps/desktop/src/locales/zh_CN/messages.json +++ b/apps/desktop/src/locales/zh_CN/messages.json @@ -1024,7 +1024,7 @@ "message": "无效的验证码" }, "invalidEmailOrVerificationCode": { - "message": "Invalid email or verification code" + "message": "无效的电子邮箱或验证码" }, "continue": { "message": "继续" @@ -4619,12 +4619,12 @@ "message": "输入多个电子邮箱(使用逗号分隔)。" }, "emailsRequiredChangeAccessType": { - "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + "message": "电子邮箱验证要求至少有一个电子邮箱地址。要移除所有电子邮箱,请更改上面的访问类型。" }, "emailPlaceholder": { "message": "user@bitwarden.com, user@acme.com" }, "userVerificationFailed": { - "message": "User verification failed." + "message": "用户验证失败。" } } From b4235110b00e6c950ef945d893e4c176784b4a3a Mon Sep 17 00:00:00 2001 From: "bw-ghapp[bot]" <178206702+bw-ghapp[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 10:34:48 +0000 Subject: [PATCH 65/89] Autosync the updated translations (#19131) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/web/src/locales/af/messages.json | 15 +++++++++ apps/web/src/locales/ar/messages.json | 15 +++++++++ apps/web/src/locales/az/messages.json | 27 ++++++++++++---- apps/web/src/locales/be/messages.json | 15 +++++++++ apps/web/src/locales/bg/messages.json | 35 +++++++++++++++------ apps/web/src/locales/bn/messages.json | 15 +++++++++ apps/web/src/locales/bs/messages.json | 15 +++++++++ apps/web/src/locales/ca/messages.json | 15 +++++++++ apps/web/src/locales/cs/messages.json | 21 +++++++++++-- apps/web/src/locales/cy/messages.json | 15 +++++++++ apps/web/src/locales/da/messages.json | 15 +++++++++ apps/web/src/locales/de/messages.json | 21 +++++++++++-- apps/web/src/locales/el/messages.json | 15 +++++++++ apps/web/src/locales/en_GB/messages.json | 15 +++++++++ apps/web/src/locales/en_IN/messages.json | 15 +++++++++ apps/web/src/locales/eo/messages.json | 15 +++++++++ apps/web/src/locales/es/messages.json | 15 +++++++++ apps/web/src/locales/et/messages.json | 15 +++++++++ apps/web/src/locales/eu/messages.json | 15 +++++++++ apps/web/src/locales/fa/messages.json | 15 +++++++++ apps/web/src/locales/fi/messages.json | 15 +++++++++ apps/web/src/locales/fil/messages.json | 15 +++++++++ apps/web/src/locales/fr/messages.json | 15 +++++++++ apps/web/src/locales/gl/messages.json | 15 +++++++++ apps/web/src/locales/he/messages.json | 15 +++++++++ apps/web/src/locales/hi/messages.json | 15 +++++++++ apps/web/src/locales/hr/messages.json | 15 +++++++++ apps/web/src/locales/hu/messages.json | 15 +++++++++ apps/web/src/locales/id/messages.json | 15 +++++++++ apps/web/src/locales/it/messages.json | 15 +++++++++ apps/web/src/locales/ja/messages.json | 15 +++++++++ apps/web/src/locales/ka/messages.json | 15 +++++++++ apps/web/src/locales/km/messages.json | 15 +++++++++ apps/web/src/locales/kn/messages.json | 15 +++++++++ apps/web/src/locales/ko/messages.json | 15 +++++++++ apps/web/src/locales/lv/messages.json | 15 +++++++++ apps/web/src/locales/ml/messages.json | 15 +++++++++ apps/web/src/locales/mr/messages.json | 15 +++++++++ apps/web/src/locales/my/messages.json | 15 +++++++++ apps/web/src/locales/nb/messages.json | 15 +++++++++ apps/web/src/locales/ne/messages.json | 15 +++++++++ apps/web/src/locales/nl/messages.json | 15 +++++++++ apps/web/src/locales/nn/messages.json | 15 +++++++++ apps/web/src/locales/or/messages.json | 15 +++++++++ apps/web/src/locales/pl/messages.json | 15 +++++++++ apps/web/src/locales/pt_BR/messages.json | 15 +++++++++ apps/web/src/locales/pt_PT/messages.json | 15 +++++++++ apps/web/src/locales/ro/messages.json | 15 +++++++++ apps/web/src/locales/ru/messages.json | 15 +++++++++ apps/web/src/locales/si/messages.json | 15 +++++++++ apps/web/src/locales/sk/messages.json | 15 +++++++++ apps/web/src/locales/sl/messages.json | 15 +++++++++ apps/web/src/locales/sr_CS/messages.json | 15 +++++++++ apps/web/src/locales/sr_CY/messages.json | 15 +++++++++ apps/web/src/locales/sv/messages.json | 15 +++++++++ apps/web/src/locales/ta/messages.json | 15 +++++++++ apps/web/src/locales/te/messages.json | 15 +++++++++ apps/web/src/locales/th/messages.json | 15 +++++++++ apps/web/src/locales/tr/messages.json | 15 +++++++++ apps/web/src/locales/uk/messages.json | 15 +++++++++ apps/web/src/locales/vi/messages.json | 15 +++++++++ apps/web/src/locales/zh_CN/messages.json | 39 ++++++++++++++++-------- apps/web/src/locales/zh_TW/messages.json | 15 +++++++++ 63 files changed, 979 insertions(+), 34 deletions(-) diff --git a/apps/web/src/locales/af/messages.json b/apps/web/src/locales/af/messages.json index 72666452d86..1e12ff7be2d 100644 --- a/apps/web/src/locales/af/messages.json +++ b/apps/web/src/locales/af/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, diff --git a/apps/web/src/locales/ar/messages.json b/apps/web/src/locales/ar/messages.json index 67fb72af9f9..9ceeb66bf59 100644 --- a/apps/web/src/locales/ar/messages.json +++ b/apps/web/src/locales/ar/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, diff --git a/apps/web/src/locales/az/messages.json b/apps/web/src/locales/az/messages.json index a97c11ea852..7d25a8a86e0 100644 --- a/apps/web/src/locales/az/messages.json +++ b/apps/web/src/locales/az/messages.json @@ -4609,16 +4609,16 @@ "message": "İrəliləyiş yüklənir" }, "reviewingMemberData": { - "message": "Reviewing member data..." + "message": "Üzv veriləri incələnir..." }, "analyzingPasswords": { - "message": "Analyzing passwords..." + "message": "Parollar təhlil edilir..." }, "calculatingRisks": { - "message": "Calculating risks..." + "message": "Risklər hesablanır..." }, "generatingReports": { - "message": "Generating reports..." + "message": "Hesabatlar yaradılır..." }, "compilingInsightsProgress": { "message": "Compiling insights..." @@ -6456,7 +6456,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "verifyYourEmailToViewThisSend": { - "message": "Verify your email to view this Send", + "message": "Bu \"Send\"ə baxmaq üçün e-poçtunuzu doğrulayın", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "viewSendHiddenEmailWarning": { @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "Bütün $GB$ GB-lıq şifrələnmiş anbar sahənizi istifadə etmisiniz. Faylları saxlaya bilmək üçün daha çox anbar sahəsi əlavə edin." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Kimlər baxa bilər" }, @@ -12989,7 +13004,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendExpiresOn": { - "message": "This Send expires at $TIME$ on $DATE$", + "message": "Bu \"Send\"in müddəti bitir: $TIME$ $DATE$", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", "placeholders": { "time": { diff --git a/apps/web/src/locales/be/messages.json b/apps/web/src/locales/be/messages.json index 91fce4817d0..a0d13b0f5db 100644 --- a/apps/web/src/locales/be/messages.json +++ b/apps/web/src/locales/be/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, diff --git a/apps/web/src/locales/bg/messages.json b/apps/web/src/locales/bg/messages.json index 484e6fd5a1b..caef0b4869b 100644 --- a/apps/web/src/locales/bg/messages.json +++ b/apps/web/src/locales/bg/messages.json @@ -4338,7 +4338,7 @@ } }, "automaticallyConfirmedUserId": { - "message": "Automatically confirmed user $ID$.", + "message": "Автоматично потвърден потребител: $ID$.", "placeholders": { "id": { "content": "$1", @@ -6148,19 +6148,19 @@ "message": "Приемам тези рискове и промени в политиката" }, "autoConfirmEnabledByAdmin": { - "message": "Turned on Automatic user confirmation setting" + "message": "Настройката за автоматично потвърждаване на потребители е включена" }, "autoConfirmDisabledByAdmin": { - "message": "Turned off Automatic user confirmation setting" + "message": "Настройката за автоматично потвърждаване на потребители е изключена" }, "autoConfirmEnabledByPortal": { - "message": "Added Automatic user confirmation policy" + "message": "Добавена е политика за автоматично потвърждаване на потребителите" }, "autoConfirmDisabledByPortal": { - "message": "Removed Automatic user confirmation policy" + "message": "Премахната е политика за автоматично потвърждаване на потребителите" }, "system": { - "message": "System" + "message": "Система" }, "personalOwnership": { "message": "Индивидуално притежание" @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "Използвали сте всичките си $GB$ GB от наличното си място за съхранение на шифровани данни. Ако искате да продължите да добавяте файлове, добавете повече място за съхранение." }, + "extensionPromptHeading": { + "message": "Инсталирайте добавката за по-лесен достъп до трезора си" + }, + "extensionPromptBody": { + "message": "Когато добавката е инсталирана, Битуорден ще бъде винаги лесно достъпен във всички уеб сайтове. С нея можете да попълвате паролите автоматично и да се вписвате с едно щракване на мишката." + }, + "extensionPromptImageAlt": { + "message": "Уеб браузър показващ добавката на Битуорден с елементи за автоматично попълване за текущата уеб страница." + }, + "skip": { + "message": "Пропускане" + }, + "downloadExtension": { + "message": "Сваляне на добавката" + }, "whoCanView": { "message": "Кой може да преглежда" }, @@ -12917,16 +12932,16 @@ "message": "Неправилна парола за Изпращане" }, "vaultWelcomeDialogTitle": { - "message": "You're in! Welcome to Bitwarden" + "message": "Влязохте! Добре дошли в Битуорден!" }, "vaultWelcomeDialogDescription": { - "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + "message": "Съхранявайте всичките си пароли и лични данни в трезора си в Битуорден. Нека Ви разведем!" }, "vaultWelcomeDialogPrimaryCta": { - "message": "Start tour" + "message": "Начало на обиколката" }, "vaultWelcomeDialogDismissCta": { - "message": "Skip" + "message": "Пропускане" }, "sendPasswordHelperText": { "message": "Хората ще трябва да въведат паролата, за да видят това Изпращане", diff --git a/apps/web/src/locales/bn/messages.json b/apps/web/src/locales/bn/messages.json index 3cc5f01c689..7dd3573e960 100644 --- a/apps/web/src/locales/bn/messages.json +++ b/apps/web/src/locales/bn/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, diff --git a/apps/web/src/locales/bs/messages.json b/apps/web/src/locales/bs/messages.json index effcfd3062b..8aea40fb633 100644 --- a/apps/web/src/locales/bs/messages.json +++ b/apps/web/src/locales/bs/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, diff --git a/apps/web/src/locales/ca/messages.json b/apps/web/src/locales/ca/messages.json index 52a0f9cdd44..9af40c9f946 100644 --- a/apps/web/src/locales/ca/messages.json +++ b/apps/web/src/locales/ca/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, diff --git a/apps/web/src/locales/cs/messages.json b/apps/web/src/locales/cs/messages.json index 300b1b583b7..469ccc0a072 100644 --- a/apps/web/src/locales/cs/messages.json +++ b/apps/web/src/locales/cs/messages.json @@ -1925,7 +1925,7 @@ "message": "Ověřovací aplikace" }, "authenticatorAppDescV2": { - "message": "Zadejte kód vygenerovaný ověřovací aplikací, jako je Autentikátor Bitwarden.", + "message": "Zadejte kód vygenerovaný ověřovací aplikací, jako je Bitwarden Authenticator.", "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, "yubiKeyTitleV2": { @@ -2704,7 +2704,7 @@ "message": "Pokračovat na bitwarden.com?" }, "twoStepContinueToBitwardenUrlDesc": { - "message": "Autentikátor Bitwarden umožňuje ukládat ověřovací klíče a generovat TOTP kódy pro 2-fázové ověřování. Další informace naleznete na stránkách bitwarden.com" + "message": "Bitwarden Authenticator umožňuje ukládat ověřovací klíče a generovat TOTP kódy pro 2-fázové ověřování. Další informace naleznete na stránkách bitwarden.com" }, "twoStepAuthenticatorScanCodeV2": { "message": "Naskenujte QR kód pomocí Vaší ověřovací aplikace nebo zadejte klíč." @@ -7418,7 +7418,7 @@ "message": "Neplatný ověřovací kód" }, "invalidEmailOrVerificationCode": { - "message": "Invalid email or verification code" + "message": "Neplatný e-mail nebo ověřovací kód" }, "keyConnectorDomain": { "message": "Doména Key Connectoru" @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "Využili jste celých $GB$ GB Vašeho šifrovaného úložiště. Chcete-li pokračovat v ukládání souborů, přidejte další úložiště." }, + "extensionPromptHeading": { + "message": "Získejte rozšíření pro snadný přístup k trezoru" + }, + "extensionPromptBody": { + "message": "S nainstalovaným rozšířením prohlížeče budete mít Bitwarden všude online. Budou se vyplňovat hesla, takže se můžete přihlásit do svých účtů jediným klepnutím." + }, + "extensionPromptImageAlt": { + "message": "Webový prohlížeč zobrazující rozšíření Bitwarden s položkami automatického vyplňování aktuální webové stránky." + }, + "skip": { + "message": "Přeskočit" + }, + "downloadExtension": { + "message": "Nainstalovat rozšíření" + }, "whoCanView": { "message": "Kdo může zobrazit" }, diff --git a/apps/web/src/locales/cy/messages.json b/apps/web/src/locales/cy/messages.json index 9fd6bb263ed..d1252309bfc 100644 --- a/apps/web/src/locales/cy/messages.json +++ b/apps/web/src/locales/cy/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, diff --git a/apps/web/src/locales/da/messages.json b/apps/web/src/locales/da/messages.json index 1f89b9f62e5..11b3dc87b89 100644 --- a/apps/web/src/locales/da/messages.json +++ b/apps/web/src/locales/da/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, diff --git a/apps/web/src/locales/de/messages.json b/apps/web/src/locales/de/messages.json index e76dc238b5e..6ea7e3b91a2 100644 --- a/apps/web/src/locales/de/messages.json +++ b/apps/web/src/locales/de/messages.json @@ -7418,7 +7418,7 @@ "message": "Ungültiger Verifizierungscode" }, "invalidEmailOrVerificationCode": { - "message": "Invalid email or verification code" + "message": "E-Mail oder Verifizierungscode ungültig" }, "keyConnectorDomain": { "message": "Key Connector-Domain" @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "Du hast die gesamten $GB$ GB deines verschlüsselten Speichers verwendet. Um mit dem Speichern von Dateien fortzufahren, füge mehr Speicher hinzu." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Wer kann das sehen" }, @@ -12890,7 +12905,7 @@ "message": "Gib mehrere E-Mail-Adressen ein, indem du sie mit einem Komma trennst." }, "emailsRequiredChangeAccessType": { - "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + "message": "E-Mail-Verifizierung erfordert mindestens eine E-Mail-Adresse. Ändere den Zugriffstyp oben, um alle E-Mails zu entfernen." }, "emailPlaceholder": { "message": "benutzer@bitwarden.com, benutzer@acme.com" @@ -12985,7 +13000,7 @@ "message": "Beim Aktualisieren deiner Zahlungsmethode ist ein Fehler aufgetreten." }, "sendPasswordInvalidAskOwner": { - "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "message": "Ungültiges Passwort. Frage den Absender nach dem Passwort, das benötigt wird, um auf dieses Send zuzugreifen.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendExpiresOn": { diff --git a/apps/web/src/locales/el/messages.json b/apps/web/src/locales/el/messages.json index 9700ec80b68..8b4bc6fc581 100644 --- a/apps/web/src/locales/el/messages.json +++ b/apps/web/src/locales/el/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, diff --git a/apps/web/src/locales/en_GB/messages.json b/apps/web/src/locales/en_GB/messages.json index 3920f2d2be6..4990efea695 100644 --- a/apps/web/src/locales/en_GB/messages.json +++ b/apps/web/src/locales/en_GB/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, diff --git a/apps/web/src/locales/en_IN/messages.json b/apps/web/src/locales/en_IN/messages.json index f9b75b283c3..24414fca4e6 100644 --- a/apps/web/src/locales/en_IN/messages.json +++ b/apps/web/src/locales/en_IN/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, diff --git a/apps/web/src/locales/eo/messages.json b/apps/web/src/locales/eo/messages.json index 4b39004d896..2d3f8d29a29 100644 --- a/apps/web/src/locales/eo/messages.json +++ b/apps/web/src/locales/eo/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, diff --git a/apps/web/src/locales/es/messages.json b/apps/web/src/locales/es/messages.json index d3b884663dd..6d4d75e20fc 100644 --- a/apps/web/src/locales/es/messages.json +++ b/apps/web/src/locales/es/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, diff --git a/apps/web/src/locales/et/messages.json b/apps/web/src/locales/et/messages.json index c312f096f1b..448103ed594 100644 --- a/apps/web/src/locales/et/messages.json +++ b/apps/web/src/locales/et/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, diff --git a/apps/web/src/locales/eu/messages.json b/apps/web/src/locales/eu/messages.json index aa8a65b1141..3376ef33f63 100644 --- a/apps/web/src/locales/eu/messages.json +++ b/apps/web/src/locales/eu/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, diff --git a/apps/web/src/locales/fa/messages.json b/apps/web/src/locales/fa/messages.json index 215af9c7512..25b2a61c256 100644 --- a/apps/web/src/locales/fa/messages.json +++ b/apps/web/src/locales/fa/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, diff --git a/apps/web/src/locales/fi/messages.json b/apps/web/src/locales/fi/messages.json index ff37dcbdfb1..f562a98c60c 100644 --- a/apps/web/src/locales/fi/messages.json +++ b/apps/web/src/locales/fi/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, diff --git a/apps/web/src/locales/fil/messages.json b/apps/web/src/locales/fil/messages.json index 85e4d95320e..af1a0105c83 100644 --- a/apps/web/src/locales/fil/messages.json +++ b/apps/web/src/locales/fil/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, diff --git a/apps/web/src/locales/fr/messages.json b/apps/web/src/locales/fr/messages.json index 09c5fe03d34..65a62b5a12d 100644 --- a/apps/web/src/locales/fr/messages.json +++ b/apps/web/src/locales/fr/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "Vous avez utilisé tous les $GB$ Go de votre stockage chiffré. Pour continuer à stocker des fichiers, ajoutez plus de stockage." }, + "extensionPromptHeading": { + "message": "Obtenir l'extension pour un accès facile au coffre" + }, + "extensionPromptBody": { + "message": "Avec l'extension de navigateur installée, vous emmènerez Bitwarden partout en ligne. Il remplira les mots de passe, faisant en sorte que vous puissiez vous connecter à vos comptes en un seul clic." + }, + "extensionPromptImageAlt": { + "message": "Un navigateur web montrant l'extension Bitwarden avec des éléments de saisie automatique pour la page web actuelle." + }, + "skip": { + "message": "Ignorer" + }, + "downloadExtension": { + "message": "Télécharger l'extension" + }, "whoCanView": { "message": "Qui peut afficher" }, diff --git a/apps/web/src/locales/gl/messages.json b/apps/web/src/locales/gl/messages.json index dccfaa04c64..17426524033 100644 --- a/apps/web/src/locales/gl/messages.json +++ b/apps/web/src/locales/gl/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, diff --git a/apps/web/src/locales/he/messages.json b/apps/web/src/locales/he/messages.json index 1dcbb3addcf..7685aaae5a1 100644 --- a/apps/web/src/locales/he/messages.json +++ b/apps/web/src/locales/he/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, diff --git a/apps/web/src/locales/hi/messages.json b/apps/web/src/locales/hi/messages.json index 3ed164386a1..1fad1304650 100644 --- a/apps/web/src/locales/hi/messages.json +++ b/apps/web/src/locales/hi/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, diff --git a/apps/web/src/locales/hr/messages.json b/apps/web/src/locales/hr/messages.json index 7a7135cd2b2..c67344da799 100644 --- a/apps/web/src/locales/hr/messages.json +++ b/apps/web/src/locales/hr/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, diff --git a/apps/web/src/locales/hu/messages.json b/apps/web/src/locales/hu/messages.json index f6f580b7120..ed38bd4b0e7 100644 --- a/apps/web/src/locales/hu/messages.json +++ b/apps/web/src/locales/hu/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "A titkosított tárhely összes $GB$ mérete felhasználásra került. A fájlok tárolásának folytatásához adjunk hozzá további tárhelyet." }, + "extensionPromptHeading": { + "message": "Szerezzük be a kiterjesztést a széf könnyű eléréséhez." + }, + "extensionPromptBody": { + "message": "A böngésző kiterjesztés telepítésével a Bitwardent mindenhová magunkkal vihetjük. Kitölti a jelszavakat, így egyetlen kattintással bejelentkezhetünk a fiókjainkba." + }, + "extensionPromptImageAlt": { + "message": "Egy webböngésző, amely a Bitwarden kiterjesztést jeleníti meg az aktuális weboldal automatikus kitöltési elemeivel." + }, + "skip": { + "message": "Kihagyás" + }, + "downloadExtension": { + "message": "Kiterjesztés letöltése" + }, "whoCanView": { "message": "Ki láthatja" }, diff --git a/apps/web/src/locales/id/messages.json b/apps/web/src/locales/id/messages.json index fb6a4908684..c1967096299 100644 --- a/apps/web/src/locales/id/messages.json +++ b/apps/web/src/locales/id/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, diff --git a/apps/web/src/locales/it/messages.json b/apps/web/src/locales/it/messages.json index f53262992fe..ac04390751d 100644 --- a/apps/web/src/locales/it/messages.json +++ b/apps/web/src/locales/it/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "Hai usato tutti i $GB$ GB del tuo spazio di archiviazione crittografato. Per archiviare altri file, aggiungi altro spazio." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Chi può visualizzare" }, diff --git a/apps/web/src/locales/ja/messages.json b/apps/web/src/locales/ja/messages.json index 2b620ed1114..b3b5a975ec0 100644 --- a/apps/web/src/locales/ja/messages.json +++ b/apps/web/src/locales/ja/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, diff --git a/apps/web/src/locales/ka/messages.json b/apps/web/src/locales/ka/messages.json index 3e527d955f3..39a28d2a0c7 100644 --- a/apps/web/src/locales/ka/messages.json +++ b/apps/web/src/locales/ka/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, diff --git a/apps/web/src/locales/km/messages.json b/apps/web/src/locales/km/messages.json index d615154225d..4e6a8265ad6 100644 --- a/apps/web/src/locales/km/messages.json +++ b/apps/web/src/locales/km/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, diff --git a/apps/web/src/locales/kn/messages.json b/apps/web/src/locales/kn/messages.json index 51ed6353e01..85e79eba91d 100644 --- a/apps/web/src/locales/kn/messages.json +++ b/apps/web/src/locales/kn/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, diff --git a/apps/web/src/locales/ko/messages.json b/apps/web/src/locales/ko/messages.json index a07134231bb..273bdddadab 100644 --- a/apps/web/src/locales/ko/messages.json +++ b/apps/web/src/locales/ko/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, diff --git a/apps/web/src/locales/lv/messages.json b/apps/web/src/locales/lv/messages.json index e50b76d1a4a..26f960a29d7 100644 --- a/apps/web/src/locales/lv/messages.json +++ b/apps/web/src/locales/lv/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, diff --git a/apps/web/src/locales/ml/messages.json b/apps/web/src/locales/ml/messages.json index d7e686fba73..c700f329c65 100644 --- a/apps/web/src/locales/ml/messages.json +++ b/apps/web/src/locales/ml/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, diff --git a/apps/web/src/locales/mr/messages.json b/apps/web/src/locales/mr/messages.json index 4f030d2368d..1fc1ef73f4b 100644 --- a/apps/web/src/locales/mr/messages.json +++ b/apps/web/src/locales/mr/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, diff --git a/apps/web/src/locales/my/messages.json b/apps/web/src/locales/my/messages.json index d615154225d..4e6a8265ad6 100644 --- a/apps/web/src/locales/my/messages.json +++ b/apps/web/src/locales/my/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, diff --git a/apps/web/src/locales/nb/messages.json b/apps/web/src/locales/nb/messages.json index 103a439220e..6c463a61e64 100644 --- a/apps/web/src/locales/nb/messages.json +++ b/apps/web/src/locales/nb/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, diff --git a/apps/web/src/locales/ne/messages.json b/apps/web/src/locales/ne/messages.json index dbf4643236d..9ef82cc799c 100644 --- a/apps/web/src/locales/ne/messages.json +++ b/apps/web/src/locales/ne/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, diff --git a/apps/web/src/locales/nl/messages.json b/apps/web/src/locales/nl/messages.json index 023a1b775a1..e50966e3ca1 100644 --- a/apps/web/src/locales/nl/messages.json +++ b/apps/web/src/locales/nl/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "Je hebt alle $GB$ GB aan versleutelde opslag gebruikt. Voeg meer opslagruimte toe om door te gaan met het opslaan van bestanden." }, + "extensionPromptHeading": { + "message": "Gebruik de extensie voor eenvoudige toegang tot je kluis" + }, + "extensionPromptBody": { + "message": "Met de browserextensie kun je Bitwarden overal online gebruiken. Het invullen van wachtwoorden, zodat je met één klik op je accounts kunt inloggen." + }, + "extensionPromptImageAlt": { + "message": "Een webbrowser die de Bitwarden-extensie toont met automatisch invullen voor de huidige webpagina." + }, + "skip": { + "message": "Overslaan" + }, + "downloadExtension": { + "message": "Extensie downloaden" + }, "whoCanView": { "message": "Wie kan weergeven" }, diff --git a/apps/web/src/locales/nn/messages.json b/apps/web/src/locales/nn/messages.json index 13f608da89d..f87cf97aa65 100644 --- a/apps/web/src/locales/nn/messages.json +++ b/apps/web/src/locales/nn/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, diff --git a/apps/web/src/locales/or/messages.json b/apps/web/src/locales/or/messages.json index d615154225d..4e6a8265ad6 100644 --- a/apps/web/src/locales/or/messages.json +++ b/apps/web/src/locales/or/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, diff --git a/apps/web/src/locales/pl/messages.json b/apps/web/src/locales/pl/messages.json index 7b5df541186..82f18f25c31 100644 --- a/apps/web/src/locales/pl/messages.json +++ b/apps/web/src/locales/pl/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, diff --git a/apps/web/src/locales/pt_BR/messages.json b/apps/web/src/locales/pt_BR/messages.json index eb8864d4e6c..73b3994fab3 100644 --- a/apps/web/src/locales/pt_BR/messages.json +++ b/apps/web/src/locales/pt_BR/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "Você usou todos os $GB$ GB do seu armazenamento criptografado. Para continuar armazenando arquivos, adicione mais armazenamento." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Quem pode visualizar" }, diff --git a/apps/web/src/locales/pt_PT/messages.json b/apps/web/src/locales/pt_PT/messages.json index eb4573b336c..55b35c60155 100644 --- a/apps/web/src/locales/pt_PT/messages.json +++ b/apps/web/src/locales/pt_PT/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "Utilizou os $GB$ GB do seu armazenamento encriptado. Para continuar a guardar ficheiros, adicione mais espaço de armazenamento." }, + "extensionPromptHeading": { + "message": "Obtenha a extensão para aceder facilmente ao seu cofre" + }, + "extensionPromptBody": { + "message": "Com a extensão do navegador instalada, terá o Bitwarden sempre disponível online. Esta preencherá automaticamente as palavras-passe, para que possa iniciar sessão nas suas contas com um único clique." + }, + "extensionPromptImageAlt": { + "message": "Um navegador web a apresentar a extensão Bitwarden com itens de preenchimento automático para a página atual." + }, + "skip": { + "message": "Saltar" + }, + "downloadExtension": { + "message": "Transferir extensão" + }, "whoCanView": { "message": "Quem pode ver" }, diff --git a/apps/web/src/locales/ro/messages.json b/apps/web/src/locales/ro/messages.json index f0b67e015cc..8fe372fbc52 100644 --- a/apps/web/src/locales/ro/messages.json +++ b/apps/web/src/locales/ro/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, diff --git a/apps/web/src/locales/ru/messages.json b/apps/web/src/locales/ru/messages.json index 0314006ab1e..6687b59fc86 100644 --- a/apps/web/src/locales/ru/messages.json +++ b/apps/web/src/locales/ru/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "Вы использовали все $GB$ вашего зашифрованного хранилища. Чтобы продолжить хранение файлов, добавьте дополнительное хранилище." }, + "extensionPromptHeading": { + "message": "Установите расширение для удобного доступа к хранилищу" + }, + "extensionPromptBody": { + "message": "Установив расширение для браузера, вы сможете использовать Bitwarden везде, где есть интернет. Оно будет вводить пароли, так что вы сможете входить в свои аккаунты одним щелчком мыши." + }, + "extensionPromptImageAlt": { + "message": "Браузер, отображающий расширение Bitwarden с элементами автозаполнения для текущей веб-страницы." + }, + "skip": { + "message": "Пропустить" + }, + "downloadExtension": { + "message": "Скачать расширение" + }, "whoCanView": { "message": "Кто может просматривать" }, diff --git a/apps/web/src/locales/si/messages.json b/apps/web/src/locales/si/messages.json index 3603a36246e..3bc5fa153d9 100644 --- a/apps/web/src/locales/si/messages.json +++ b/apps/web/src/locales/si/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, diff --git a/apps/web/src/locales/sk/messages.json b/apps/web/src/locales/sk/messages.json index 79e8b40f918..6a25d6d0f3f 100644 --- a/apps/web/src/locales/sk/messages.json +++ b/apps/web/src/locales/sk/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "Použili ste všetkých $GB$ GB vášho šifrovaného úložiska. Ak chcete uložiť ďalšie súbory, pridajte viac úložiska." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, diff --git a/apps/web/src/locales/sl/messages.json b/apps/web/src/locales/sl/messages.json index e5a622ca157..4553f68c54b 100644 --- a/apps/web/src/locales/sl/messages.json +++ b/apps/web/src/locales/sl/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, diff --git a/apps/web/src/locales/sr_CS/messages.json b/apps/web/src/locales/sr_CS/messages.json index f869499d685..24ff0eb2f9f 100644 --- a/apps/web/src/locales/sr_CS/messages.json +++ b/apps/web/src/locales/sr_CS/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, diff --git a/apps/web/src/locales/sr_CY/messages.json b/apps/web/src/locales/sr_CY/messages.json index e416df73247..f0878f973c2 100644 --- a/apps/web/src/locales/sr_CY/messages.json +++ b/apps/web/src/locales/sr_CY/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, diff --git a/apps/web/src/locales/sv/messages.json b/apps/web/src/locales/sv/messages.json index 079dce0e3a7..77771bb166d 100644 --- a/apps/web/src/locales/sv/messages.json +++ b/apps/web/src/locales/sv/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "Du har använt alla $GB$ GB av din krypterade lagring. För att fortsätta lagra filer, lägg till mer lagringsutrymme." }, + "extensionPromptHeading": { + "message": "Skaffa tillägget för enkel åtkomst till valv" + }, + "extensionPromptBody": { + "message": "Med webbläsartillägget installerat tar du Bitwarden överallt på nätet. Det fyller i lösenord, så att du kan logga in på dina konton med ett enda klick." + }, + "extensionPromptImageAlt": { + "message": "En webbläsare som visar Bitwarden-tillägget med autofyll objekt för den aktuella webbsidan." + }, + "skip": { + "message": "Hoppa över" + }, + "downloadExtension": { + "message": "Ladda ner tillägg" + }, "whoCanView": { "message": "Vem kan se" }, diff --git a/apps/web/src/locales/ta/messages.json b/apps/web/src/locales/ta/messages.json index a1eb60d67c1..757a8158097 100644 --- a/apps/web/src/locales/ta/messages.json +++ b/apps/web/src/locales/ta/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, diff --git a/apps/web/src/locales/te/messages.json b/apps/web/src/locales/te/messages.json index d615154225d..4e6a8265ad6 100644 --- a/apps/web/src/locales/te/messages.json +++ b/apps/web/src/locales/te/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, diff --git a/apps/web/src/locales/th/messages.json b/apps/web/src/locales/th/messages.json index ab53147de00..2eb0a00f8fc 100644 --- a/apps/web/src/locales/th/messages.json +++ b/apps/web/src/locales/th/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, diff --git a/apps/web/src/locales/tr/messages.json b/apps/web/src/locales/tr/messages.json index efd186d721a..260514462cb 100644 --- a/apps/web/src/locales/tr/messages.json +++ b/apps/web/src/locales/tr/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Kim görebilir" }, diff --git a/apps/web/src/locales/uk/messages.json b/apps/web/src/locales/uk/messages.json index 71780d2faa9..7dc407ad5e6 100644 --- a/apps/web/src/locales/uk/messages.json +++ b/apps/web/src/locales/uk/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Хто може переглядати" }, diff --git a/apps/web/src/locales/vi/messages.json b/apps/web/src/locales/vi/messages.json index 369a87111d4..3671c9e8ab5 100644 --- a/apps/web/src/locales/vi/messages.json +++ b/apps/web/src/locales/vi/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, diff --git a/apps/web/src/locales/zh_CN/messages.json b/apps/web/src/locales/zh_CN/messages.json index 275e11a1c70..ff23e62c5d8 100644 --- a/apps/web/src/locales/zh_CN/messages.json +++ b/apps/web/src/locales/zh_CN/messages.json @@ -4338,7 +4338,7 @@ } }, "automaticallyConfirmedUserId": { - "message": "Automatically confirmed user $ID$.", + "message": "自动确认了用户 $ID$。", "placeholders": { "id": { "content": "$1", @@ -6148,19 +6148,19 @@ "message": "我接受这些风险和策略更新" }, "autoConfirmEnabledByAdmin": { - "message": "Turned on Automatic user confirmation setting" + "message": "启用了自动用户确认设置" }, "autoConfirmDisabledByAdmin": { - "message": "Turned off Automatic user confirmation setting" + "message": "停用了自动用户确认设置" }, "autoConfirmEnabledByPortal": { - "message": "Added Automatic user confirmation policy" + "message": "添加了自动用户确认策略" }, "autoConfirmDisabledByPortal": { - "message": "Removed Automatic user confirmation policy" + "message": "禁用了自动用户确认策略" }, "system": { - "message": "System" + "message": "系统" }, "personalOwnership": { "message": "禁用个人密码库" @@ -6761,7 +6761,7 @@ "message": "1 份邀请未发送" }, "bulkReinviteFailureDescription": { - "message": "向 $TOTAL$ 位成员中的 $COUNT$ 位发送邀请时发生错误。请尝试重新发送,如果问题仍然存在,", + "message": "向 $TOTAL$ 位成员中的 $COUNT$ 位发送邀请时发生错误。请尝试再次发送,如果问题仍然存在,", "placeholders": { "count": { "content": "$1", @@ -9171,7 +9171,7 @@ "message": "查看全部" }, "showingPortionOfTotal": { - "message": "显示 $PORTION$ / $TOTAL$", + "message": "显示 $TOTAL$ 中的 $PORTION$", "placeholders": { "portion": { "content": "$1", @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "您已使用了全部的 $GB$ GB 加密存储空间。要继续存储文件,请添加更多存储空间。" }, + "extensionPromptHeading": { + "message": "获取扩展以便轻松访问密码库" + }, + "extensionPromptBody": { + "message": "安装浏览器扩展后,您可以随时随地在线使用 Bitwarden。它会自动填写密码,只需单击一下即可登录您的账户。" + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "跳过" + }, + "downloadExtension": { + "message": "下载扩展" + }, "whoCanView": { "message": "谁可以查看" }, @@ -12917,16 +12932,16 @@ "message": "无效的 Send 密码" }, "vaultWelcomeDialogTitle": { - "message": "You're in! Welcome to Bitwarden" + "message": "您已成功加入!欢迎使用 Bitwarden" }, "vaultWelcomeDialogDescription": { - "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + "message": "将您的所有密码和个人信息存储在你的 Bitwarden 密码库中。我们将带您熟悉一下。" }, "vaultWelcomeDialogPrimaryCta": { - "message": "Start tour" + "message": "开始导览" }, "vaultWelcomeDialogDismissCta": { - "message": "Skip" + "message": "跳过" }, "sendPasswordHelperText": { "message": "个人需要输入密码才能查看此 Send", diff --git a/apps/web/src/locales/zh_TW/messages.json b/apps/web/src/locales/zh_TW/messages.json index aedc802f241..31099edc763 100644 --- a/apps/web/src/locales/zh_TW/messages.json +++ b/apps/web/src/locales/zh_TW/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "您已用完全部 $GB$ GB 的加密儲存空間。如需繼續儲存檔案,請增加儲存空間。" }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "誰可以檢視" }, From a90d74c32c6e93aee4780b0a7175af4e24e24013 Mon Sep 17 00:00:00 2001 From: "bw-ghapp[bot]" <178206702+bw-ghapp[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 04:35:06 -0600 Subject: [PATCH 66/89] Autosync the updated translations (#19130) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/browser/src/_locales/az/messages.json | 8 ++--- apps/browser/src/_locales/bg/messages.json | 2 +- apps/browser/src/_locales/cs/messages.json | 2 +- apps/browser/src/_locales/de/messages.json | 4 +-- apps/browser/src/_locales/it/messages.json | 32 ++++++++--------- apps/browser/src/_locales/uk/messages.json | 34 +++++++++---------- apps/browser/src/_locales/zh_CN/messages.json | 8 ++--- 7 files changed, 45 insertions(+), 45 deletions(-) diff --git a/apps/browser/src/_locales/az/messages.json b/apps/browser/src/_locales/az/messages.json index 6572c2d09d9..eb3135599f4 100644 --- a/apps/browser/src/_locales/az/messages.json +++ b/apps/browser/src/_locales/az/messages.json @@ -3087,7 +3087,7 @@ } }, "durationTimeHours": { - "message": "$HOURS$ hours", + "message": "$HOURS$ saat", "placeholders": { "hours": { "content": "$1", @@ -3096,7 +3096,7 @@ } }, "sendCreatedDescriptionV2": { - "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "message": "Bu Send keçidini kopyala və paylaş. Send, keçidə sahib olan hər kəs üçün növbəti $TIME$ ərzində əlçatan olacaq.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", "placeholders": { "time": { @@ -3106,7 +3106,7 @@ } }, "sendCreatedDescriptionPassword": { - "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "message": "Bu Send keçidini kopyala və paylaş. Send, keçidə və ayarladığınız parola sahib olan hər kəs üçün növbəti $TIME$ ərzində əlçatan olacaq.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", "placeholders": { "time": { @@ -3116,7 +3116,7 @@ } }, "sendCreatedDescriptionEmail": { - "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "message": "Bu Send keçidini kopyala və paylaş. Qeyd etdiyiniz şəxslər buna növbəti $TIME$ ərzində baxa bilər.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", "placeholders": { "time": { diff --git a/apps/browser/src/_locales/bg/messages.json b/apps/browser/src/_locales/bg/messages.json index 4f2f26bade8..275dd21d0e9 100644 --- a/apps/browser/src/_locales/bg/messages.json +++ b/apps/browser/src/_locales/bg/messages.json @@ -2861,7 +2861,7 @@ "message": "Илюстрация на списък с елементи за вписване, които са в риск." }, "welcomeDialogGraphicAlt": { - "message": "Illustration of the layout of the Bitwarden vault page." + "message": "Илюстрация на оформлението на страницата с трезора в Битуорден." }, "generatePasswordSlideDesc": { "message": "Генерирайте бързо сложна и уникална парола от менюто за автоматично попълване на Битуорден, на уеб сайта, който е в риск.", diff --git a/apps/browser/src/_locales/cs/messages.json b/apps/browser/src/_locales/cs/messages.json index 9106d0518db..efadf781fc8 100644 --- a/apps/browser/src/_locales/cs/messages.json +++ b/apps/browser/src/_locales/cs/messages.json @@ -1691,7 +1691,7 @@ "message": "Ověřovací aplikace" }, "authenticatorAppDescV2": { - "message": "Zadejte kód vygenerovaný ověřovací aplikací, jako je Autentikátor Bitwarden.", + "message": "Zadejte kód vygenerovaný ověřovací aplikací, jako je Bitwarden Authenticator.", "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, "yubiKeyTitleV2": { diff --git a/apps/browser/src/_locales/de/messages.json b/apps/browser/src/_locales/de/messages.json index 6d301950e03..8c33da9ae79 100644 --- a/apps/browser/src/_locales/de/messages.json +++ b/apps/browser/src/_locales/de/messages.json @@ -897,7 +897,7 @@ "message": "Ungültiger Verifizierungscode" }, "invalidEmailOrVerificationCode": { - "message": "Invalid email or verification code" + "message": "E-Mail oder Verifizierungscode ungültig" }, "valueCopied": { "message": "$VALUE$ kopiert", @@ -6167,7 +6167,7 @@ "message": "Gib mehrere E-Mail-Adressen ein, indem du sie mit einem Komma trennst." }, "emailsRequiredChangeAccessType": { - "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + "message": "E-Mail-Verifizierung erfordert mindestens eine E-Mail-Adresse. Ändere den Zugriffstyp oben, um alle E-Mails zu entfernen." }, "emailPlaceholder": { "message": "benutzer@bitwarden.com, benutzer@acme.com" diff --git a/apps/browser/src/_locales/it/messages.json b/apps/browser/src/_locales/it/messages.json index d66dfe7bfba..f1d704c6d48 100644 --- a/apps/browser/src/_locales/it/messages.json +++ b/apps/browser/src/_locales/it/messages.json @@ -574,10 +574,10 @@ "message": "Gli elementi archiviati compariranno qui e saranno esclusi dai risultati di ricerca e suggerimenti di autoriempimento." }, "itemArchiveToast": { - "message": "Item archived" + "message": "Elemento archiviato" }, "itemUnarchivedToast": { - "message": "Item unarchived" + "message": "Elemento estratto dall'archivio" }, "archiveItem": { "message": "Archivia elemento" @@ -897,7 +897,7 @@ "message": "Codice di verifica non valido" }, "invalidEmailOrVerificationCode": { - "message": "Invalid email or verification code" + "message": "Codice di verifica non valido" }, "valueCopied": { "message": "$VALUE$ copiata", @@ -2861,7 +2861,7 @@ "message": "Illustrazione di una lista di login a rischio." }, "welcomeDialogGraphicAlt": { - "message": "Illustration of the layout of the Bitwarden vault page." + "message": "Illustrazione del layout di pagina della cassaforte Bitwarden." }, "generatePasswordSlideDesc": { "message": "Genera rapidamente una parola d'accesso forte e unica con il menu' di riempimento automatico Bitwarden nel sito a rischio.", @@ -3087,7 +3087,7 @@ } }, "durationTimeHours": { - "message": "$HOURS$ hours", + "message": "$HOURS$ ore", "placeholders": { "hours": { "content": "$1", @@ -3096,7 +3096,7 @@ } }, "sendCreatedDescriptionV2": { - "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "message": "Copia e condividi questo link di Send. Sarà disponibile a chiunque abbia il link per $TIME$.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", "placeholders": { "time": { @@ -3106,7 +3106,7 @@ } }, "sendCreatedDescriptionPassword": { - "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "message": "Copia e condividi questo link di Send. Sarà disponibile a chiunque abbia link e password per $TIME$.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", "placeholders": { "time": { @@ -3116,7 +3116,7 @@ } }, "sendCreatedDescriptionEmail": { - "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "message": "Copia e condividi questo link Send: potrà essere visualizzato dalle persone che hai specificato per $TIME$.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", "placeholders": { "time": { @@ -6006,7 +6006,7 @@ "message": "Numero di carta" }, "errorCannotDecrypt": { - "message": "Error: Cannot decrypt" + "message": "Errore: impossibile decrittare" }, "removeMasterPasswordForOrgUserKeyConnector": { "message": "La tua organizzazione non utilizza più le password principali per accedere a Bitwarden. Per continuare, verifica l'organizzazione e il dominio." @@ -6146,10 +6146,10 @@ "message": "Perché vedo questo avviso?" }, "items": { - "message": "Items" + "message": "Elementi" }, "searchResults": { - "message": "Search results" + "message": "Risultati di ricerca" }, "resizeSideNavigation": { "message": "Ridimensiona la navigazione laterale" @@ -6167,22 +6167,22 @@ "message": "Inserisci più indirizzi email separandoli con virgole." }, "emailsRequiredChangeAccessType": { - "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + "message": "La verifica via email richiede almeno un indirizzo email. Per rimuovere tutte le email, modifica il tipo di accesso qui sopra." }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, "downloadBitwardenApps": { - "message": "Download Bitwarden apps" + "message": "Scarica l'app Bitwarden" }, "emailProtected": { - "message": "Email protected" + "message": "Email protetta" }, "sendPasswordHelperText": { - "message": "Individuals will need to enter the password to view this Send", + "message": "I destinatari dovranno inserire la password per visualizzare questo Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "userVerificationFailed": { - "message": "User verification failed." + "message": "Verifica dell'utente non riuscita." } } diff --git a/apps/browser/src/_locales/uk/messages.json b/apps/browser/src/_locales/uk/messages.json index 541a60b8ff8..143dc8037fd 100644 --- a/apps/browser/src/_locales/uk/messages.json +++ b/apps/browser/src/_locales/uk/messages.json @@ -897,7 +897,7 @@ "message": "Недійсний код підтвердження" }, "invalidEmailOrVerificationCode": { - "message": "Invalid email or verification code" + "message": "Недійсна е-пошта або код підтвердження" }, "valueCopied": { "message": "$VALUE$ скопійовано", @@ -1144,7 +1144,7 @@ "message": "Натисніть на запис у режимі перегляду сховища для автозаповнення" }, "clickToAutofill": { - "message": "Натисніть запис у пропозиціях для автозаповнення" + "message": "Натиснути запис у пропозиціях для автозаповнення" }, "clearClipboard": { "message": "Очистити буфер обміну", @@ -2055,7 +2055,7 @@ "message": "Е-пошта" }, "emails": { - "message": "Е-пошти" + "message": "Адреси е-пошти" }, "phone": { "message": "Телефон" @@ -2861,7 +2861,7 @@ "message": "Ілюстрація списку ризикованих записів." }, "welcomeDialogGraphicAlt": { - "message": "Illustration of the layout of the Bitwarden vault page." + "message": "Ілюстрація макету сторінки сховища Bitwarden." }, "generatePasswordSlideDesc": { "message": "Швидко згенеруйте надійний, унікальний пароль через меню автозаповнення Bitwarden на сайті з ризикованим паролем.", @@ -3087,7 +3087,7 @@ } }, "durationTimeHours": { - "message": "$HOURS$ hours", + "message": "$HOURS$ годин", "placeholders": { "hours": { "content": "$1", @@ -3096,7 +3096,7 @@ } }, "sendCreatedDescriptionV2": { - "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "message": "Скопіюйте посилання на це відправлення і поділіться ним. Відправлення буде доступне за посиланням усім протягом $TIME$.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", "placeholders": { "time": { @@ -3106,7 +3106,7 @@ } }, "sendCreatedDescriptionPassword": { - "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "message": "Скопіюйте посилання на це відправлення і поділіться ним. Відправлення буде доступне за посиланням і встановленим вами паролем усім протягом $TIME$.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", "placeholders": { "time": { @@ -3116,7 +3116,7 @@ } }, "sendCreatedDescriptionEmail": { - "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "message": "Скопіюйте посилання на це відправлення і поділіться ним. Його зможуть переглядати зазначені вами користувачі протягом $TIME$.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", "placeholders": { "time": { @@ -3398,7 +3398,7 @@ "message": "Не вдалося розблокувати за допомогою ключа доступу. Повторіть спробу або скористайтеся іншим способом розблокування." }, "noPrfCredentialsAvailable": { - "message": "Немає ключів доступу з підтримкою PRF, доступних для розблокування. Спочатку увійдіть з ключем доступу." + "message": "Немає ключів доступу з підтримкою PRF, доступних для розблокування. Спочатку ввійдіть з ключем доступу." }, "decryptionError": { "message": "Помилка розшифрування" @@ -4734,7 +4734,7 @@ "message": "Запропоновані записи" }, "autofillSuggestionsTip": { - "message": "Зберегти дані входу цього сайту для автозаповнення" + "message": "Збережіть дані входу цього сайту для автозаповнення" }, "yourVaultIsEmpty": { "message": "Ваше сховище порожнє" @@ -4776,7 +4776,7 @@ } }, "moreOptionsLabelNoPlaceholder": { - "message": "Більше опцій" + "message": "Інші варіанти" }, "moreOptionsTitle": { "message": "Інші можливості – $ITEMNAME$", @@ -5710,7 +5710,7 @@ "message": "Дуже широке" }, "narrow": { - "message": "Вузький" + "message": "Вузьке" }, "sshKeyWrongPassword": { "message": "Ви ввели неправильний пароль." @@ -6158,16 +6158,16 @@ "message": "Хто може переглядати" }, "specificPeople": { - "message": "Певні люди" + "message": "Певні користувачі" }, "emailVerificationDesc": { - "message": "Після того, як ви поділитеся цим посиланням на відправлення, особам необхідно буде підтвердити свої е-пошти за допомогою коду, щоб переглянути це відправлення." + "message": "Після того, як ви поділитеся посиланням на це відправлення, користувачі мають підтвердити свою е-пошту за допомогою коду, щоб переглянути його." }, "enterMultipleEmailsSeparatedByComma": { "message": "Введіть декілька адрес е-пошти, розділяючи їх комою." }, "emailsRequiredChangeAccessType": { - "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + "message": "Для підтвердження адреси електронної пошти потрібна щонайменше одна адреса. Щоб вилучити всі адреси електронної пошти, змініть тип доступу вище." }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" @@ -6179,10 +6179,10 @@ "message": "Е-пошту захищено" }, "sendPasswordHelperText": { - "message": "Особам необхідно ввести пароль для перегляду цього відправлення", + "message": "Користувачі мають ввести пароль для перегляду цього відправлення", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "userVerificationFailed": { - "message": "User verification failed." + "message": "Не вдалося перевірити користувача." } } diff --git a/apps/browser/src/_locales/zh_CN/messages.json b/apps/browser/src/_locales/zh_CN/messages.json index 5fc0b632676..c27d1a8bb24 100644 --- a/apps/browser/src/_locales/zh_CN/messages.json +++ b/apps/browser/src/_locales/zh_CN/messages.json @@ -897,7 +897,7 @@ "message": "无效的验证码" }, "invalidEmailOrVerificationCode": { - "message": "Invalid email or verification code" + "message": "无效的电子邮箱或验证码" }, "valueCopied": { "message": "$VALUE$ 已复制", @@ -2861,7 +2861,7 @@ "message": "存在风险的登录列表示意图。" }, "welcomeDialogGraphicAlt": { - "message": "Illustration of the layout of the Bitwarden vault page." + "message": "Bitwarden 密码库页面布局示意图。" }, "generatePasswordSlideDesc": { "message": "在存在风险的网站上,使用 Bitwarden 自动填充菜单快速生成强大且唯一的密码。", @@ -6167,7 +6167,7 @@ "message": "输入多个电子邮箱(使用逗号分隔)。" }, "emailsRequiredChangeAccessType": { - "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + "message": "电子邮箱验证要求至少有一个电子邮箱地址。要移除所有电子邮箱,请更改上面的访问类型。" }, "emailPlaceholder": { "message": "user@bitwarden.com, user@acme.com" @@ -6183,6 +6183,6 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "userVerificationFailed": { - "message": "User verification failed." + "message": "用户验证失败。" } } From e6c4998b7ccd1568c4518e43de6586afa3d1f227 Mon Sep 17 00:00:00 2001 From: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> Date: Mon, 23 Feb 2026 11:48:40 +0000 Subject: [PATCH 67/89] Bumped client version(s) --- apps/browser/package.json | 2 +- apps/browser/src/manifest.json | 2 +- apps/browser/src/manifest.v3.json | 2 +- apps/cli/package.json | 2 +- apps/web/package.json | 2 +- package-lock.json | 6 +++--- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/browser/package.json b/apps/browser/package.json index 53103643374..fa3da23991e 100644 --- a/apps/browser/package.json +++ b/apps/browser/package.json @@ -1,6 +1,6 @@ { "name": "@bitwarden/browser", - "version": "2026.1.1", + "version": "2026.2.0", "scripts": { "build": "npm run build:chrome", "build:bit": "npm run build:bit:chrome", diff --git a/apps/browser/src/manifest.json b/apps/browser/src/manifest.json index c2e0b422985..54a10e34b12 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": "Bitwarden", - "version": "2026.1.1", + "version": "2026.2.0", "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 603d3e06ba7..206a300236c 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": "Bitwarden", - "version": "2026.1.1", + "version": "2026.2.0", "description": "__MSG_extDesc__", "default_locale": "en", "author": "Bitwarden Inc.", diff --git a/apps/cli/package.json b/apps/cli/package.json index 6c27267054f..a5b3a00ec4e 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": "2026.1.0", + "version": "2026.2.0", "keywords": [ "bitwarden", "password", diff --git a/apps/web/package.json b/apps/web/package.json index ad778b03778..844ac1f12b5 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@bitwarden/web-vault", - "version": "2026.2.0", + "version": "2026.2.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 47ba7456b0a..0c2a38a9cc2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -191,11 +191,11 @@ }, "apps/browser": { "name": "@bitwarden/browser", - "version": "2026.1.1" + "version": "2026.2.0" }, "apps/cli": { "name": "@bitwarden/cli", - "version": "2026.1.0", + "version": "2026.2.0", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@koa/multer": "4.0.0", @@ -445,7 +445,7 @@ }, "apps/web": { "name": "@bitwarden/web-vault", - "version": "2026.2.0" + "version": "2026.2.1" }, "libs/admin-console": { "name": "@bitwarden/admin-console", From cf32250d7b947804fa196f719574d913888fd3f6 Mon Sep 17 00:00:00 2001 From: bmbitwarden Date: Mon, 23 Feb 2026 09:09:05 -0500 Subject: [PATCH 68/89] PM-7853 implemented hide send based on config setting (#18831) --- apps/web/src/app/layouts/user-layout.component.html | 4 +++- apps/web/src/app/layouts/user-layout.component.ts | 8 +++++++- apps/web/src/app/oss-routing.module.ts | 9 +++++++++ 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/apps/web/src/app/layouts/user-layout.component.html b/apps/web/src/app/layouts/user-layout.component.html index 10f569e2558..57b8cf047c4 100644 --- a/apps/web/src/app/layouts/user-layout.component.html +++ b/apps/web/src/app/layouts/user-layout.component.html @@ -3,7 +3,9 @@ - + @if (sendEnabled$ | async) { + + } diff --git a/apps/web/src/app/layouts/user-layout.component.ts b/apps/web/src/app/layouts/user-layout.component.ts index 33bce661c65..6af7b0639e5 100644 --- a/apps/web/src/app/layouts/user-layout.component.ts +++ b/apps/web/src/app/layouts/user-layout.component.ts @@ -4,12 +4,13 @@ import { CommonModule } from "@angular/common"; import { Component, OnInit, Signal } from "@angular/core"; import { toSignal } from "@angular/core/rxjs-interop"; import { RouterModule } from "@angular/router"; -import { Observable, switchMap } from "rxjs"; +import { map, Observable, switchMap } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { PasswordManagerLogo } from "@bitwarden/assets/svg"; import { canAccessEmergencyAccess } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; +import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; @@ -42,6 +43,11 @@ export class UserLayoutComponent implements OnInit { protected hasFamilySponsorshipAvailable$: Observable; protected showSponsoredFamilies$: Observable; protected showSubscription$: Observable; + protected readonly sendEnabled$: Observable = this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => this.policyService.policyAppliesToUser$(PolicyType.DisableSend, userId)), + map((isDisabled) => !isDisabled), + ); protected consolidatedSessionTimeoutComponent$: Observable; constructor( diff --git a/apps/web/src/app/oss-routing.module.ts b/apps/web/src/app/oss-routing.module.ts index 932d0b8119b..a5fe3f5d627 100644 --- a/apps/web/src/app/oss-routing.module.ts +++ b/apps/web/src/app/oss-routing.module.ts @@ -1,5 +1,6 @@ import { NgModule } from "@angular/core"; import { Route, RouterModule, Routes } from "@angular/router"; +import { map } from "rxjs"; import { organizationPolicyGuard } from "@bitwarden/angular/admin-console/guards"; import { AuthenticationTimeoutComponent } from "@bitwarden/angular/auth/components/authentication-timeout.component"; @@ -50,6 +51,7 @@ import { NewDeviceVerificationComponent, } from "@bitwarden/auth/angular"; import { canAccessEmergencyAccess } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { AnonLayoutWrapperComponent, AnonLayoutWrapperData } from "@bitwarden/components"; import { LockComponent, RemovePasswordComponent } from "@bitwarden/key-management-ui"; @@ -641,6 +643,13 @@ const routes: Routes = [ path: "sends", component: SendComponent, data: { titleId: "send" } satisfies RouteDataProperties, + canActivate: [ + organizationPolicyGuard((userId, _configService, policyService) => + policyService + .policyAppliesToUser$(PolicyType.DisableSend, userId) + .pipe(map((policyApplies) => !policyApplies)), + ), + ], }, { path: "sm-landing", From 4fea6300734573aa447fbe16e939f7ccb6b211c9 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Mon, 23 Feb 2026 15:16:30 +0100 Subject: [PATCH 69/89] Fix user crypto management module not being imported correctly (#19133) --- apps/web/src/app/core/core.module.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/web/src/app/core/core.module.ts b/apps/web/src/app/core/core.module.ts index b3afb8ca984..d270162f99d 100644 --- a/apps/web/src/app/core/core.module.ts +++ b/apps/web/src/app/core/core.module.ts @@ -126,6 +126,7 @@ import { SessionTimeoutSettingsComponentService, } from "@bitwarden/key-management-ui"; import { SerializedMemoryStorageService } from "@bitwarden/storage-core"; +import { UserCryptoManagementModule } from "@bitwarden/user-crypto-management"; import { DefaultSshImportPromptService, SshImportPromptService } from "@bitwarden/vault"; import { WebOrganizationInviteService } from "@bitwarden/web-vault/app/auth/core/services/organization-invite/web-organization-invite.service"; import { WebVaultPremiumUpgradePromptService } from "@bitwarden/web-vault/app/vault/services/web-premium-upgrade-prompt.service"; @@ -497,7 +498,7 @@ const safeProviders: SafeProvider[] = [ @NgModule({ declarations: [], - imports: [CommonModule, JslibServicesModule, GeneratorServicesModule], + imports: [CommonModule, JslibServicesModule, UserCryptoManagementModule, GeneratorServicesModule], // Do not register your dependency here! Add it to the typesafeProviders array using the helper function providers: safeProviders, }) From 494dd7d329adc102e34678ca59734877b2faaf9b Mon Sep 17 00:00:00 2001 From: Vijay Oommen Date: Mon, 23 Feb 2026 08:17:46 -0600 Subject: [PATCH 70/89] [PM-31833] Split mark as critical and assign tasks (#18843) --- .../new-applications-dialog.component.ts | 145 ++++++++++-------- 1 file changed, 80 insertions(+), 65 deletions(-) diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/application-review-dialog/new-applications-dialog.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/application-review-dialog/new-applications-dialog.component.ts index 5b9cea436a0..13018ba6884 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/application-review-dialog/new-applications-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/application-review-dialog/new-applications-dialog.component.ts @@ -9,8 +9,8 @@ import { Signal, signal, } from "@angular/core"; -import { takeUntilDestroyed, toSignal } from "@angular/core/rxjs-interop"; -import { catchError, EMPTY, from, switchMap, take } from "rxjs"; +import { toSignal } from "@angular/core/rxjs-interop"; +import { firstValueFrom } from "rxjs"; import { ApplicationHealthReportDetail, @@ -238,6 +238,12 @@ export class NewApplicationsDialogComponent { // Checks if there are selected applications and proceeds to assign tasks async handleMarkAsCritical() { + if (this.markingAsCritical()) { + return; // Prevent double-click + } + + this.markingAsCritical.set(true); + if (this.selectedApplications().size === 0) { const confirmed = await this.dialogService.openSimpleDialog({ title: { key: "confirmNoSelectedCriticalApplicationsTitle" }, @@ -246,25 +252,11 @@ export class NewApplicationsDialogComponent { }); if (!confirmed) { + this.markingAsCritical.set(false); return; } } - // Skip the assign tasks view if there are no new unassigned at-risk cipher IDs - if (this.newUnassignedAtRiskCipherIds().length === 0) { - this.handleAssignTasks(); - } else { - this.currentView.set(DialogView.AssignTasks); - } - } - - // Saves the application review and assigns tasks for unassigned at-risk ciphers - protected handleAssignTasks() { - if (this.saving()) { - return; // Prevent double-click - } - this.saving.set(true); - const reviewedDate = new Date(); const updatedApplications = this.dialogParams.newApplications.map((app) => { const isCritical = this.selectedApplications().has(app.applicationName); @@ -276,56 +268,79 @@ export class NewApplicationsDialogComponent { }); // Save the application review dates and critical markings - this.dataService - .saveApplicationReviewStatus(updatedApplications) - .pipe( - takeUntilDestroyed(this.destroyRef), // Satisfy eslint rule - take(1), - switchMap(() => { - // Assign password change tasks for unassigned at-risk ciphers for critical applications - return from( - this.securityTasksService.requestPasswordChangeForCriticalApplications( - this.dialogParams.organizationId, - this.newUnassignedAtRiskCipherIds(), - ), - ); - }), - catchError((error: unknown) => { - if (error instanceof ErrorResponse && error.statusCode === 404) { - this.toastService.showToast({ - message: this.i18nService.t("mustBeOrganizationOwnerAdmin"), - variant: "error", - title: this.i18nService.t("error"), - }); + try { + await firstValueFrom(this.dataService.saveApplicationReviewStatus(updatedApplications)); - this.saving.set(false); - return EMPTY; - } - - this.logService.error( - "[NewApplicationsDialog] Failed to save application review or assign tasks", - error, - ); - this.saving.set(false); - this.toastService.showToast({ - variant: "error", - title: this.i18nService.t("errorSavingReviewStatus"), - message: this.i18nService.t("pleaseTryAgain"), - }); - - this.saving.set(false); - return EMPTY; - }), - ) - .subscribe(() => { - this.toastService.showToast({ - variant: "success", - title: this.i18nService.t("applicationReviewSaved"), - message: this.i18nService.t("newApplicationsReviewed"), - }); - this.saving.set(false); - this.handleAssigningCompleted(); + this.toastService.showToast({ + variant: "success", + title: this.i18nService.t("applicationReviewSaved"), + message: this.i18nService.t("newApplicationsReviewed"), }); + + // If there are no unassigned at-risk ciphers, we can complete immediately. Otherwise, navigate to the assign tasks view. + if (this.newUnassignedAtRiskCipherIds().length === 0) { + this.handleAssigningCompleted(); + } else { + this.currentView.set(DialogView.AssignTasks); + } + } catch (error: unknown) { + this.logService.error( + "[NewApplicationsDialog] Failed to save application review status", + error, + ); + + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorSavingReviewStatus"), + message: this.i18nService.t("pleaseTryAgain"), + }); + } finally { + this.markingAsCritical.set(false); + } + } + + // Saves the application review and assigns tasks for unassigned at-risk ciphers + protected async handleAssignTasks() { + if (this.saving()) { + return; // Prevent double-click + } + this.saving.set(true); + + try { + await this.securityTasksService.requestPasswordChangeForCriticalApplications( + this.dialogParams.organizationId, + this.newUnassignedAtRiskCipherIds(), + ); + + this.toastService.showToast({ + variant: "success", + title: this.i18nService.t("success"), + message: this.i18nService.t("notifiedMembers"), + }); + + // close the dialog + this.handleAssigningCompleted(); + } catch (error: unknown) { + if (error instanceof ErrorResponse && error.statusCode === 404) { + this.toastService.showToast({ + message: this.i18nService.t("mustBeOrganizationOwnerAdmin"), + variant: "error", + title: this.i18nService.t("error"), + }); + + return; + } + + this.logService.error("[NewApplicationsDialog] Failed to assign tasks", error); + + this.toastService.showToast({ + message: this.i18nService.t("unexpectedError"), + variant: "error", + title: this.i18nService.t("error"), + }); + } finally { + this.saving.set(false); + } } /** From 2af9396766a962b56705094bfeb2dd4e642295ea Mon Sep 17 00:00:00 2001 From: Leslie Tilton <23057410+Banrion@users.noreply.github.com> Date: Mon, 23 Feb 2026 08:47:16 -0600 Subject: [PATCH 71/89] Initial bitwarden team core docs (#19048) --- .../bit-common/src/dirt/docs/README.md | 73 ++++ .../src/dirt/docs/documentation-structure.md | 277 +++++++++++++ .../src/dirt/docs/getting-started.md | 96 +++++ .../src/dirt/docs/integration-guide.md | 378 ++++++++++++++++++ 4 files changed, 824 insertions(+) create mode 100644 bitwarden_license/bit-common/src/dirt/docs/README.md create mode 100644 bitwarden_license/bit-common/src/dirt/docs/documentation-structure.md create mode 100644 bitwarden_license/bit-common/src/dirt/docs/getting-started.md create mode 100644 bitwarden_license/bit-common/src/dirt/docs/integration-guide.md diff --git a/bitwarden_license/bit-common/src/dirt/docs/README.md b/bitwarden_license/bit-common/src/dirt/docs/README.md new file mode 100644 index 00000000000..f07f5c8b44c --- /dev/null +++ b/bitwarden_license/bit-common/src/dirt/docs/README.md @@ -0,0 +1,73 @@ +# DIRT Team Documentation + +**Location:** `bitwarden_license/bit-common/src/dirt/docs/` +**Purpose:** Overview of DIRT team documentation with navigation to detailed guides + +--- + +## 🎯 Start Here + +**New to the DIRT team?** → [Getting Started](./getting-started.md) + +**Looking for something specific?** + +- **"What should I read for my task?"** → [Getting Started](./getting-started.md) +- **"How are docs organized?"** → [Documentation Structure](./documentation-structure.md) +- **"How do I implement a feature?"** → [Playbooks](./playbooks/) +- **"What are the coding standards?"** → [Standards](./standards/) +- **"How do services integrate with components?"** → [Integration Guide](./integration-guide.md) + +--- + +## 📁 What's in This Folder + +| Document/Folder | Purpose | +| -------------------------------------------------------------- | ------------------------------------------------- | +| **[getting-started.md](./getting-started.md)** | Navigation hub - what to read for your task | +| **[documentation-structure.md](./documentation-structure.md)** | Complete structure guide - how docs are organized | +| **[integration-guide.md](./integration-guide.md)** | Service ↔ Component integration patterns | +| **[playbooks/](./playbooks/)** | Step-by-step implementation guides | +| **[standards/](./standards/)** | Team coding and documentation standards | +| **[access-intelligence/](./access-intelligence/)** | Migration guides and architecture comparisons | + +--- + +## 🏗️ DIRT Team Features + +The DIRT team (Data, Insights, Reporting & Tooling) owns: + +- **Access Intelligence** - Organization security reporting and password health +- **Organization Integrations** - Third-party integrations +- **External Reports** - Organization reports (weak passwords, member access, etc.) +- **Phishing Detection** - Browser-based phishing detection + +**Documentation is organized by package:** + +- **bit-common** - Platform-agnostic services (work on all platforms) +- **bit-web** - Angular web components (web client only) +- **bit-browser** - Browser extension components + +For detailed feature documentation locations, see [Getting Started](./getting-started.md). + +--- + +## 📝 Creating New Documentation + +**Before creating new docs, follow these steps:** + +1. **Read the standards:** [Documentation Standards](./standards/documentation-standards.md) +2. **Check for overlaps:** Review existing docs to avoid duplication +3. **Follow the playbook:** [Documentation Playbook](./playbooks/documentation-playbook.md) +4. **Update navigation:** Add to [getting-started.md](./getting-started.md) if it's a primary entry point +5. **Update this README:** If adding a new category or top-level document + +**For detailed guidance on where to place docs, see:** + +- [Documentation Standards § Document Location Rules](./standards/documentation-standards.md#document-location-rules) +- [Documentation Structure](./documentation-structure.md) + +--- + +**Document Version:** 1.0 +**Last Updated:** 2026-02-17 +**Maintainer:** DIRT Team diff --git a/bitwarden_license/bit-common/src/dirt/docs/documentation-structure.md b/bitwarden_license/bit-common/src/dirt/docs/documentation-structure.md new file mode 100644 index 00000000000..7d7e20b5d31 --- /dev/null +++ b/bitwarden_license/bit-common/src/dirt/docs/documentation-structure.md @@ -0,0 +1,277 @@ +# DIRT Team Documentation Structure + +**Purpose:** Navigation guide for all DIRT team documentation organized by team/feature hierarchy + +--- + +## 📁 Documentation Organization + +DIRT team documentation follows a **team/feature** hierarchy organized across multiple locations based on separation of concerns: + +### Team-Level Documentation + +**Location:** `bitwarden_license/bit-common/src/dirt/docs/` + +**Scope:** Applies to all DIRT features (Access Intelligence, Phishing Detection, etc.) + +**Contains:** + +- Team playbooks (service, component, documentation) +- Team coding standards +- Integration guides +- Getting started guide + +### Feature-Level Documentation + +**Pattern:** Feature docs live **next to the feature code**, not in the team `docs/` folder. + +**Location:** `dirt/[feature]/docs/` + +**Examples:** + +- **Access Intelligence:** `dirt/access-intelligence/v2/docs/` (or `dirt/access-intelligence/docs/` for current version) +- **Phishing Detection (future):** `dirt/phishing-detection/docs/` + +**Feature docs contain:** + +- Feature-specific architecture +- Feature-specific implementation guides +- Feature-specific patterns + +**Exception:** Migration/transition documentation can live in team `docs/` as **team-level knowledge**. Example: `docs/access-intelligence/` contains migration guides from v1 to v2, which is team-level context about the transition, not feature-specific architecture. + +### 1. Services & Architecture (Platform-Agnostic) + +**Pattern:** `bitwarden_license/bit-common/src/dirt/[feature]/docs/` + +**Purpose:** Feature-specific documentation lives next to the feature code + +**Example for Access Intelligence:** + +- Location: `dirt/access-intelligence/v2/docs/` (for v2 architecture) +- Contains: Architecture docs, implementation guides specific to that version + +**Note:** Team-level migration docs may live in `docs/access-intelligence/` as team knowledge about the transition between versions. + +### 2. Components (Angular-Specific) + +**Pattern:** `bitwarden_license/bit-web/src/app/dirt/[feature]/docs/` + +**Purpose:** Angular-specific UI components for web client only + +**Example for Access Intelligence:** + +- Location: `dirt/access-intelligence/docs/` +- Contains: Component inventory, migration guides, Storybook + +--- + +## 🎯 Where to Start? + +**For navigation guidance (what to read), see:** [getting-started.md](./getting-started.md) + +This document focuses on **how** the documentation is organized, not **what** to read. + +--- + +## 🗂️ Complete File Structure + +``` +# ============================================================================ +# SERVICES & ARCHITECTURE (bit-common) +# Platform-agnostic - Used by web, desktop, browser, CLI +# ============================================================================ + +bitwarden_license/bit-common/src/dirt/ +├── docs/ ← TEAM-LEVEL documentation only +│ ├── README.md ← Team docs overview +│ ├── getting-started.md ← Entry point for team +│ ├── documentation-structure.md ← This file +│ ├── integration-guide.md ← Service ↔ Component integration +│ │ +│ ├── playbooks/ ← Team playbooks (service, component, docs) +│ │ └── README.md ← Playbook navigation +│ │ +│ ├── standards/ ← Team coding standards +│ │ └── standards.md ← Core standards +│ │ +│ └── access-intelligence/ ← EXCEPTION: Migration guides (team knowledge) +│ ├── README.md ← Migration overview +│ ├── ... ← Migration analysis files +│ ├── architecture/ ← Migration architecture comparison +│ │ └── ... ← Architecture comparison files +│ └── implementation/ ← Implementation guides +│ └── ... ← Integration guides +│ +└── [feature]/ ← FEATURE CODE + FEATURE DOCS + └── docs/ ← Feature-specific documentation + ├── README.md ← Feature docs navigation + ├── architecture/ ← Feature architecture (lives with code) + │ └── ... ← Architecture files + └── implementation/ ← Feature implementation guides + └── ... ← Implementation guide files + +# Example for Access Intelligence v2: +bitwarden_license/bit-common/src/dirt/access-intelligence/ +├── v2/ ← V2 implementation +│ ├── services/ ← V2 services +│ ├── models/ ← V2 models +│ └── docs/ ← V2-SPECIFIC documentation +│ ├── README.md ← V2 docs overview +│ ├── architecture/ ← V2 architecture +│ │ └── ... ← Architecture files +│ └── implementation/ ← V2 implementation guides +│ └── ... ← Implementation guide files +└── v1/ ← V1 implementation (legacy) + +# ============================================================================ +# COMPONENTS (bit-web) +# Angular-specific - Web client only +# ============================================================================ + +bitwarden_license/bit-web/src/app/dirt/[feature]/ +├── docs/ ← Component documentation +│ └── README.md ← Component docs navigation +├── [component folders]/ ← Angular components +└── v2/ ← V2 components (if applicable) + +# Example for Access Intelligence: +bitwarden_license/bit-web/src/app/dirt/access-intelligence/ +├── docs/ ← Component documentation +│ ├── README.md ← Component docs navigation +│ └── ... ← Component guides +├── [components]/ ← Angular components +└── v2/ ← V2 components (if applicable) + └── ... ← V2 component files +``` + +--- + +## 🔄 When to Update This Structure + +Update this document when: + +- [ ] Adding new documentation categories +- [ ] Changing file locations +- [ ] Restructuring documentation organization + +--- + +## 📝 Architecture Decisions + +**Where decisions are tracked:** + +- **Company-wide ADRs:** Stored in the `contributing-docs` repository +- **Feature-specific decisions:** Tracked in Confluence (link to be added) +- **Local decision notes (optional):** `~/Documents/bitwarden-notes/dirt/decisions/[feature]/` for personal reference before moving to Confluence + - Example: `~/Documents/bitwarden-notes/dirt/decisions/access-intelligence/` + +**What goes in repo architecture docs:** + +- Current architecture state +- Migration plans and roadmaps +- Technical constraints +- Implementation patterns + +**What goes in Confluence:** + +- Decision discussions and rationale +- Alternative approaches considered +- Stakeholder input +- Links to Slack discussions + +--- + +## ✏️ Creating New Documentation + +**Before creating new documentation, see:** [docs/README.md](./README.md) § Documentation Best Practices + +**Key principles:** + +- **Single responsibility** - Each document should answer one question +- **Check for overlaps** - Read related docs first +- **Follow naming conventions** - See [documentation-standards.md](./standards/documentation-standards.md) +- **Cross-reference standards** - See [documentation-standards.md § Cross-Reference Standards](./standards/documentation-standards.md#cross-reference-standards) +- **Update navigation** - Add to getting-started.md if it's a primary entry point + +--- + +## 📊 Why This Structure? + +### Documentation Placement Principles + +**Team-Level Documentation (`docs/`):** + +- Applies to all DIRT features +- Playbooks, standards, getting-started guides +- Migration guides and transition documentation (team knowledge about rewrites) +- Cross-feature integration patterns + +**Feature-Level Documentation (`dirt/[feature]/docs/`):** + +- Lives **next to the feature code** +- Feature-specific architecture +- Version-specific implementation details +- Feature-specific patterns + +**Rationale:** + +- **Discoverability:** Architecture docs are found where the code lives +- **Versioning:** v1 and v2 can have separate docs directories +- **Maintainability:** Update feature docs without touching team docs +- **Clarity:** Clear separation between "what applies to all features" vs "what applies to this feature" + +### Separation of Concerns + +**Platform-Agnostic (bit-common):** + +- Services work on all platforms (web, desktop, browser, CLI) +- Domain models are platform-independent +- Architecture decisions affect all clients +- **Feature docs live with feature code:** `dirt/[feature]/docs/` + +**Angular-Specific (bit-web):** + +- Components only used in web client +- Storybook is web-only +- Angular-specific patterns (OnPush, Signals, etc.) +- **Component docs live with components:** `dirt/[feature]/docs/` + +### Benefits + +1. **Clarity:** Developers know where to look based on what they're working on +2. **Separation:** Team docs vs feature docs, Angular code vs platform-agnostic code +3. **Discoverability:** Feature docs are near feature code +4. **Maintainability:** Easier to update feature docs without affecting team docs +5. **Scalability:** Can add versioned docs (v1/, v2/) next to versioned code +6. **Migration clarity:** Team `docs/` can hold migration guides while feature `docs/` hold version-specific architecture + +--- + +## 🆘 Need Help? + +### Can't Find Documentation? + +1. **Start with getting-started.md:** [getting-started.md](./getting-started.md) + - Navigation hub for all DIRT team documentation + - Links to all major documentation categories + +2. **Check README files:** + - [Team Documentation README](./README.md) + - [Component README](/bitwarden_license/bit-web/src/app/dirt/access-intelligence/docs/README.md) + +3. **Check feature-specific docs:** + - Look in `dirt/[feature]/docs/` next to the feature code + - Example: `dirt/access-intelligence/v2/docs/` + +### Links Broken? + +- Check if file was moved +- Update cross-references following [documentation-standards.md § Cross-Reference Standards](./standards/documentation-standards.md#cross-reference-standards) +- Update navigation in README.md files + +--- + +**Document Version:** 1.0 +**Last Updated:** 2026-02-17 +**Maintainer:** DIRT Team diff --git a/bitwarden_license/bit-common/src/dirt/docs/getting-started.md b/bitwarden_license/bit-common/src/dirt/docs/getting-started.md new file mode 100644 index 00000000000..0077019fe02 --- /dev/null +++ b/bitwarden_license/bit-common/src/dirt/docs/getting-started.md @@ -0,0 +1,96 @@ +# DIRT Team - Getting Started + +**Purpose:** Navigation hub showing what documentation is available for your work + +--- + +## 🎯 DIRT Team Features + +The **DIRT team** (Data, Insights, Reporting & Tooling) owns: + +- **Access Intelligence** (formerly Risk Insights) + - Organization security reporting and password health analysis + - Location: `dirt/reports/risk-insights/` (v1 services), `bit-web/.../access-intelligence/` (UI) + - Note: `risk-insights` is the v1 codebase name for Access Intelligence + +- **Organization Integrations** + - Third-party organization integrations + - Location: `dirt/organization-integrations/` + +- **External Reports** + - Various organization reports (weak password report, member access report, etc.) + - Documentation: Coming soon + +- **Phishing Detection** + - Documentation: Coming soon + +**Note:** Access Intelligence has the most documentation as it's the first feature we're documenting comprehensively. + +--- + +## 📚 What's Available + +### Development Resources + +| Resource Type | What It Provides | Where to Find It | +| ----------------------- | ------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | +| **Playbooks** | Step-by-step implementation guides for common dev tasks | [Playbooks Hub](./playbooks/) | +| **Standards** | Coding conventions, patterns, and best practices | [Standards Hub](./standards/README.md) | +| **Architecture** | Feature architecture reviews and migration plans | [Access Intelligence Architecture](./access-intelligence/architecture/) | +| **Integration Guides** | How services and components work together | [Generic Guide](./integration-guide.md), [Access Intelligence](./access-intelligence/service-component-integration.md) | +| **Documentation Guide** | How docs are organized and where to find things | [Documentation Structure](./documentation-structure.md) | + +### Standards by Area + +| Area | Standard Document | +| ---------------------- | -------------------------------------------------------------------------- | +| **General Coding** | [Standards Hub](./standards/README.md) | +| **Services** | [Service Standards](./standards/service-standards.md) | +| **Domain Models** | [Model Standards](./standards/model-standards.md) | +| **Service Testing** | [Service Testing Standards](./standards/testing-standards-services.md) | +| **Angular Components** | [Angular Standards](./standards/angular-standards.md) | +| **Component Testing** | [Component Testing Standards](./standards/testing-standards-components.md) | +| **RxJS Patterns** | [RxJS Standards](./standards/rxjs-standards.md) | +| **Code Organization** | [Code Organization Standards](./standards/code-organization-standards.md) | +| **Documentation** | [Documentation Standards](./standards/documentation-standards.md) | + +### Playbooks by Task + +| Task | Playbook | +| ------------------------------------ | --------------------------------------------------------------------------------- | +| **Implement or refactor a service** | [Service Implementation Playbook](./playbooks/service-implementation-playbook.md) | +| **Migrate or create a UI component** | [Component Migration Playbook](./playbooks/component-migration-playbook.md) | +| **Create or update documentation** | [Documentation Playbook](./playbooks/documentation-playbook.md) | +| **Browse all playbooks** | [Playbooks Hub](./playbooks/) | + +--- + +## 🚀 Quick Reference by Task + +| What are you working on? | Start here | +| ---------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Services** (implementation, architecture, testing) | [Service Playbook](./playbooks/service-implementation-playbook.md) + [Service Standards](./standards/service-standards.md) | +| **Domain Models** (view models, query methods) | [Service Playbook](./playbooks/service-implementation-playbook.md) + [Model Standards](./standards/model-standards.md) | +| **UI Components** (Angular, migration, testing) | [Component Playbook](./playbooks/component-migration-playbook.md) + [Angular Standards](./standards/angular-standards.md) | +| **Storybook** (create or update stories) | [Component Playbook](./playbooks/component-migration-playbook.md) + [Component Testing Standards § Storybook](./standards/testing-standards-components.md#storybook-as-living-documentation) | +| **Component Tests** (Jest, OnPush, Signals) | [Component Playbook](./playbooks/component-migration-playbook.md) + [Component Testing Standards](./standards/testing-standards-components.md) | +| **Service Tests** (mocks, observables, RxJS) | [Service Playbook](./playbooks/service-implementation-playbook.md) + [Service Testing Standards](./standards/testing-standards-services.md) | +| **Documentation** (create, update, organize) | [Documentation Playbook](./playbooks/documentation-playbook.md) + [Documentation Standards](./standards/documentation-standards.md) | +| **Architecture Review** (feature planning) | [Access Intelligence Architecture](./access-intelligence/architecture/) | +| **Feature Architecture Decisions** | Document in [docs/[feature]/architecture/](./documentation-structure.md#feature-level-documentation) (decisions tracked in Confluence) | + +--- + +## 🆘 Need Help? + +**Can't find what you're looking for?** + +- **Understand how docs are organized:** See [Documentation Structure](./documentation-structure.md) +- **Browse all team documentation:** See [Team Docs README](./README.md) +- **Component-specific docs:** See [Component Docs](/bitwarden_license/bit-web/src/app/dirt/access-intelligence/docs/README.md) + +--- + +**Document Version:** 1.0 +**Last Updated:** 2026-02-17 +**Maintainer:** DIRT Team diff --git a/bitwarden_license/bit-common/src/dirt/docs/integration-guide.md b/bitwarden_license/bit-common/src/dirt/docs/integration-guide.md new file mode 100644 index 00000000000..0d7bf3db847 --- /dev/null +++ b/bitwarden_license/bit-common/src/dirt/docs/integration-guide.md @@ -0,0 +1,378 @@ +# Service ↔ Component Integration Guide + +**Purpose:** Coordination guide for features that span both platform-agnostic services (bit-common) and Angular UI components (bit-web/bit-browser) + +**Scope:** This guide applies to **any DIRT feature** requiring work in both service and component layers. For feature-specific integration patterns and detailed examples, see the feature's documentation: + +- [Access Intelligence Integration](/bitwarden_license/bit-common/src/dirt/docs/access-intelligence/service-component-integration.md) + +**Focus:** This document focuses on **coordination and handoffs** between service and component developers. For code patterns and standards, see [Standards Documentation](/bitwarden_license/bit-common/src/dirt/docs/standards/standards.md). + +--- + +## 📋 When You Need Both + +Many DIRT features require coordinated work across service AND component layers: + +| Feature Type | Service Work | Component Work | +| -------------------------- | ----------------------------- | --------------------------------- | +| **New report/data type** | Generate, persist, load data | Display data, filters, navigation | +| **New data visualization** | Aggregate/query data | Charts, cards, tables | +| **User actions** | Business logic on models | UI interactions, forms | +| **Settings/preferences** | Persist settings | Settings UI | +| **Integrations** | API communication, sync logic | Configuration UI, status display | + +--- + +## 🔄 Integration Pattern + +``` +┌─────────────────────────────────────────────────┐ +│ Component (bit-web/bit-browser) │ +│ - User interactions │ +│ - Display logic │ +│ - Converts Observables → Signals (toSignal()) │ +│ - OnPush + Signal inputs/outputs │ +├─────────────────────────────────────────────────┤ +│ Data Service (Feature-specific) │ +│ - Exposes Observable streams │ +│ - Coordinates feature data │ +│ - Delegates business logic to models │ +│ - Delegates persistence to services │ +├─────────────────────────────────────────────────┤ +│ Domain Services (bit-common) │ +│ - Business logic orchestration │ +│ - Pure transformation │ +│ - Platform-agnostic │ +├─────────────────────────────────────────────────┤ +│ View Models │ +│ - Smart models (CipherView pattern) │ +│ - Query methods: getData(), filter(), etc. │ +│ - Mutation methods: update(), delete(), etc. │ +└─────────────────────────────────────────────────┘ +``` + +**Key principle:** Services do the work, components coordinate the UI. Business logic lives in view models, not components. + +--- + +## 🔀 Service → Component Handoff + +**When:** Service implementation is complete, ready for UI integration + +### Readiness Checklist + +Before handing off to component developer, ensure: + +- [ ] **Service is complete and tested** + - [ ] Abstract defined with JSDoc + - [ ] Implementation complete + - [ ] Tests passing (`npm run test`) + - [ ] Types validated (`npm run test:types`) + +- [ ] **View models have required methods** + - [ ] Query methods for component data needs (documented) + - [ ] Mutation methods for user actions (documented) + - [ ] Methods follow naming conventions + +- [ ] **Data service exposes observables** + - [ ] Observable(s) are public and documented + - [ ] Observable emits correct view models + - [ ] Observable handles errors gracefully + +- [ ] **Component requirements documented** + - [ ] What data the component needs + - [ ] What user actions the component handles + - [ ] What the component should display + - [ ] Any performance considerations + +### Handoff Communication Template + +When handing off to component developer, provide: + +1. **What service to inject** + - Example: `FeatureDataService` + +2. **What observable(s) to use** + - Example: `data$: Observable` + - Type signature and nullability + +3. **What model methods are available** + - Query methods: `feature.getData()`, `feature.filter(criteria)` + - Mutation methods: `feature.update(data)`, `feature.delete(id)` + - Link to model documentation or JSDoc + +4. **How to integrate in component** + - Reference [Standards: Observable to Signal Conversion](/bitwarden_license/bit-common/src/dirt/docs/standards/standards.md) + - Basic pattern: inject service → convert observable to signal → use in template + +5. **Any gotchas or special considerations** + - Performance notes (large datasets, expensive operations) + - Error handling requirements + - Special states (loading, empty, error) + +### Communication Methods + +- **Slack:** Quick handoff for simple integrations +- **Jira comment:** Document handoff details on feature ticket +- **Documentation:** Update feature docs with integration examples +- **Pair session:** For complex integrations, schedule pairing + +--- + +## 🔀 Component → Service Handoff + +**When:** Component needs new data/functionality not yet available in services + +### Discovery Checklist + +Before creating a service request, identify: + +- [ ] **What's missing** + - [ ] New query method needed on view model? + - [ ] New mutation method needed on view model? + - [ ] New service needed entirely? + - [ ] New data needs to be loaded/persisted? + +- [ ] **Document the requirement clearly** + - [ ] What data the component needs (shape, type) + - [ ] What format the data should be in + - [ ] What user action triggers this need + - [ ] Performance requirements (dataset size, frequency) + +- [ ] **Assess scope** + - [ ] Is this a new method on existing model? (small change) + - [ ] Is this a new service? (medium-large change) + - [ ] Does this require API changes? (involves backend team) + +- [ ] **File appropriate ticket** + - [ ] Link to component/feature that needs it + - [ ] Link to design/mockup if applicable + - [ ] Tag service developer or tech lead + +### Handoff Communication Template + +When requesting service work, provide: + +1. **What the component needs** + - Clear description: "Component needs list of filtered items based on user criteria" + +2. **Proposed API (if you have one)** + - Example: `model.getFilteredItems(criteria): Item[]` + - This is negotiable, service developer may suggest better approach + +3. **Why (user story/context)** + - Example: "User clicks 'Show only critical' filter, UI should update to show subset" + +4. **Data format expected** + - Example: "Array of `{ id: string, name: string, isCritical: boolean }`" + - Or reference existing model type if reusing + +5. **Performance/scale considerations** + - Example: "Could be 1000+ items for large organizations" + - Helps service developer optimize + +6. **Timeline/priority** + - Is this blocking component work? + - Can component proceed with stub/mock for now? + +### Communication Methods + +- **Jira ticket:** For non-trivial work requiring tracking +- **Slack:** For quick questions or small additions +- **Planning session:** For large features requiring design discussion +- **ADR:** If architectural decision needed + +--- + +## 🤝 Collaboration Patterns + +### Pattern 1: Parallel Development + +**When to use:** Service and component work can be developed simultaneously + +**How:** + +1. Service developer creates interface/abstract first +2. Component developer uses interface with mock data +3. Both develop in parallel +4. Integration happens at the end + +**Benefits:** Faster delivery, clear contracts + +### Pattern 2: Sequential Development (Service First) + +**When to use:** Component needs complete service implementation + +**How:** + +1. Service developer implements fully +2. Service developer documents integration +3. Component developer integrates +4. Component developer provides feedback + +**Benefits:** Fewer integration issues, clearer requirements + +### Pattern 3: Sequential Development (Component First) + +**When to use:** UI/UX needs to be proven before service investment + +**How:** + +1. Component developer builds with mock data +2. Component developer documents data needs +3. Service developer implements to match needs +4. Integration and refinement + +**Benefits:** User-driven design, avoids unused service work + +### Pattern 4: Paired Development + +**When to use:** Complex integration, unclear requirements, new patterns + +**How:** + +1. Service and component developer pair on design +2. Develop together or in short iterations +3. Continuous feedback and adjustment + +**Benefits:** Fastest problem solving, shared understanding + +--- + +## 🧪 Testing Integration Points + +### Service Layer Testing + +**Service developers should test:** + +- Services return correct view models +- Observables emit expected data +- Error handling works correctly +- Performance is acceptable for expected dataset sizes + +**Reference:** [Service Implementation Playbook - Testing](/bitwarden_license/bit-common/src/dirt/docs/playbooks/service-implementation-playbook.md) + +### Component Layer Testing + +**Component developers should test:** + +- Services are correctly injected +- Observables are correctly converted to signals +- View model methods are called appropriately +- Data is displayed correctly +- User interactions trigger correct model methods + +**Reference:** [Component Migration Playbook - Testing](/bitwarden_license/bit-common/src/dirt/docs/playbooks/component-migration-playbook.md) + +### Integration Testing + +**Both should coordinate on:** + +- Full user flows work end-to-end +- Data flows correctly from service → component +- UI updates when data changes +- Error states are handled gracefully + +--- + +## 🚨 Common Integration Pitfalls + +### 1. Component Bypasses Data Service + +**Problem:** Component directly calls API services or persistence layers + +**Why it's bad:** Breaks abstraction, duplicates logic, harder to test + +**Solution:** Always go through feature's data service layer + +**Reference:** [Standards: Service Layer Pattern](/bitwarden_license/bit-common/src/dirt/docs/standards/standards.md) + +### 2. Service Returns Plain Objects + +**Problem:** Service returns `{ ... }` instead of view model instances + +**Why it's bad:** Loses model methods, breaks encapsulation, business logic leaks to components + +**Solution:** Always return view model instances with query/mutation methods + +**Reference:** [Standards: View Models](/bitwarden_license/bit-common/src/dirt/docs/standards/standards.md) + +### 3. Business Logic in Components + +**Problem:** Component implements filtering, calculations, state changes + +**Why it's bad:** Logic not reusable, harder to test, violates separation of concerns + +**Solution:** Business logic belongs in view models or domain services + +**Reference:** [Standards: Component Responsibilities](/bitwarden_license/bit-common/src/dirt/docs/standards/standards.md) + +### 4. Manual Observable Subscriptions + +**Problem:** Component uses `.subscribe()` instead of `toSignal()` + +**Why it's bad:** Memory leaks, manual cleanup needed, doesn't leverage Angular signals + +**Solution:** Use `toSignal()` for automatic cleanup and signal integration + +**Reference:** [Standards: Observable to Signal Conversion](/bitwarden_license/bit-common/src/dirt/docs/standards/standards.md) + +### 5. Unclear Handoff + +**Problem:** Service developer finishes work but doesn't communicate to component developer + +**Why it's bad:** Delays integration, component developer doesn't know work is ready + +**Solution:** Use handoff communication templates above, update Jira tickets, notify in Slack + +--- + +## 📞 Who to Contact + +### Service Questions + +- Check: [Service Implementation Playbook](/bitwarden_license/bit-common/src/dirt/docs/playbooks/service-implementation-playbook.md) +- Check: [Standards](/bitwarden_license/bit-common/src/dirt/docs/standards/standards.md) +- Ask: DIRT team service developers + +### Component Questions + +- Check: [Component Migration Playbook](/bitwarden_license/bit-common/src/dirt/docs/playbooks/component-migration-playbook.md) +- Check: [Standards](/bitwarden_license/bit-common/src/dirt/docs/standards/standards.md) +- Ask: DIRT team component developers + +### Architecture Questions + +- Check: [Architecture Docs](/bitwarden_license/bit-common/src/dirt/docs/access-intelligence/architecture/) +- Check: [Getting Started](/bitwarden_license/bit-common/src/dirt/docs/getting-started.md) +- Ask: DIRT team tech lead + +### Coordination/Process Questions + +- Ask: DIRT team lead or scrum master + +--- + +## 📚 Related Documentation + +### General Guides + +- [Getting Started](/bitwarden_license/bit-common/src/dirt/docs/getting-started.md) +- [Standards](/bitwarden_license/bit-common/src/dirt/docs/standards/standards.md) +- [Documentation Structure](/bitwarden_license/bit-common/src/dirt/docs/documentation-structure.md) + +### Implementation Playbooks + +- [Service Implementation Playbook](/bitwarden_license/bit-common/src/dirt/docs/playbooks/service-implementation-playbook.md) +- [Component Migration Playbook](/bitwarden_license/bit-common/src/dirt/docs/playbooks/component-migration-playbook.md) + +### Feature-Specific Integration Guides + +- [Access Intelligence Integration](/bitwarden_license/bit-common/src/dirt/docs/access-intelligence/service-component-integration.md) + +--- + +**Document Version:** 1.0 +**Last Updated:** 2026-02-17 +**Maintainer:** DIRT Team From 74aec0b80c96b61566149888d2e31090084079e1 Mon Sep 17 00:00:00 2001 From: Leslie Tilton <23057410+Banrion@users.noreply.github.com> Date: Mon, 23 Feb 2026 09:05:26 -0600 Subject: [PATCH 72/89] [PM-26487][PM-20112] Member Access Report - Member Cipher Client Mapping (#18774) * Added v2 version of member access reports that aggregate data client side instead of using endpoint that times out. Added feature flag. * Remove feature flag * Added avatar color to the member access report * Update icon usage * Add story book for member access report * Add icon module to member access report component * Fix test case * Update member access report service to match export of v1 version. Update test cases * Fix billing error in member access report * Add timeout to fetch organization ciphers * Handle group naming * Add cached permission text * Add memberAccessReportLoadError message * Fix member cipher mapping to deduplicate data in memory * Update log * Update storybook with deterministic data and test type * Fix avatar color default * Fix types * Address timeout cleanup --- apps/web/src/locales/en/messages.json | 3 + .../member-access-report.component.html | 20 +- .../member-access-report.component.stories.ts | 268 +++++++ .../member-access-report.component.ts | 102 ++- .../member-access-report.service.spec.ts | 670 +++++++++++++++++- .../services/member-access-report.service.ts | 464 +++++++++++- .../view/member-access-report.view.ts | 1 + 7 files changed, 1490 insertions(+), 38 deletions(-) create mode 100644 bitwarden_license/bit-web/src/app/dirt/reports/member-access-report/member-access-report.component.stories.ts diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index ef8c109bc4b..7ea2abb5d08 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -10977,6 +10977,9 @@ "memberAccessReportAuthenticationEnabledFalse": { "message": "Off" }, + "memberAccessReportLoadError": { + "message": "Failed to load the member access report. This may be due to a large organization size or network issue. Please try again or contact support if the problem persists." + }, "kdfIterationRecommends": { "message": "We recommend 600,000 or more" }, diff --git a/bitwarden_license/bit-web/src/app/dirt/reports/member-access-report/member-access-report.component.html b/bitwarden_license/bit-web/src/app/dirt/reports/member-access-report/member-access-report.component.html index 440e955a226..6769998e2c8 100644 --- a/bitwarden_license/bit-web/src/app/dirt/reports/member-access-report/member-access-report.component.html +++ b/bitwarden_license/bit-web/src/app/dirt/reports/member-access-report/member-access-report.component.html @@ -9,7 +9,7 @@ > } @@ -22,11 +22,11 @@ @if (isLoading) {
- +

{{ "loading" | i18n }}

} @else { @@ -42,7 +42,13 @@
- +
diff --git a/apps/browser/src/auth/popup/account-switching/account-switcher.component.ts b/apps/browser/src/auth/popup/account-switching/account-switcher.component.ts index ae7f66a9018..53d488192ba 100644 --- a/apps/browser/src/auth/popup/account-switching/account-switcher.component.ts +++ b/apps/browser/src/auth/popup/account-switching/account-switcher.component.ts @@ -18,6 +18,7 @@ import { AvatarModule, ButtonModule, DialogService, + IconModule, ItemModule, SectionComponent, SectionHeaderComponent, @@ -42,6 +43,7 @@ import { AccountSwitcherService } from "./services/account-switcher.service"; ButtonModule, ItemModule, AvatarModule, + IconModule, PopupPageComponent, PopupHeaderComponent, PopOutComponent, diff --git a/apps/browser/src/auth/popup/account-switching/account.component.html b/apps/browser/src/auth/popup/account-switching/account.component.html index 90770bb8d9b..d756365cc5b 100644 --- a/apps/browser/src/auth/popup/account-switching/account.component.html +++ b/apps/browser/src/auth/popup/account-switching/account.component.html @@ -32,13 +32,13 @@
- + diff --git a/apps/browser/src/auth/popup/account-switching/account.component.ts b/apps/browser/src/auth/popup/account-switching/account.component.ts index edfad2a54b3..dbc31a1f011 100644 --- a/apps/browser/src/auth/popup/account-switching/account.component.ts +++ b/apps/browser/src/auth/popup/account-switching/account.component.ts @@ -8,7 +8,7 @@ import { JslibModule } from "@bitwarden/angular/jslib.module"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { AvatarModule, ItemModule } from "@bitwarden/components"; +import { AvatarModule, IconModule, ItemModule, type BitwardenIcon } from "@bitwarden/components"; import { BiometricsService } from "@bitwarden/key-management"; import { AccountSwitcherService, AvailableAccount } from "./services/account-switcher.service"; @@ -18,7 +18,7 @@ import { AccountSwitcherService, AvailableAccount } from "./services/account-swi @Component({ selector: "auth-account", templateUrl: "account.component.html", - imports: [CommonModule, JslibModule, AvatarModule, ItemModule], + imports: [CommonModule, JslibModule, AvatarModule, IconModule, ItemModule], }) export class AccountComponent { // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals @@ -60,7 +60,7 @@ export class AccountComponent { this.loading.emit(false); } - get status() { + get status(): { text: string; icon: BitwardenIcon } { if (this.account.isActive) { return { text: this.i18nService.t("active"), icon: "bwi-check-circle" }; } diff --git a/apps/browser/src/auth/popup/settings/account-security.component.html b/apps/browser/src/auth/popup/settings/account-security.component.html index bb6b141c6c5..366f5b82790 100644 --- a/apps/browser/src/auth/popup/settings/account-security.component.html +++ b/apps/browser/src/auth/popup/settings/account-security.component.html @@ -123,7 +123,7 @@ @@ -154,13 +154,13 @@ diff --git a/apps/browser/src/auth/popup/settings/account-security.component.ts b/apps/browser/src/auth/popup/settings/account-security.component.ts index 1789feebe4e..b3bd9b842f7 100644 --- a/apps/browser/src/auth/popup/settings/account-security.component.ts +++ b/apps/browser/src/auth/popup/settings/account-security.component.ts @@ -56,6 +56,7 @@ import { DialogService, FormFieldModule, IconButtonModule, + IconModule, ItemModule, LinkModule, SectionComponent, @@ -98,6 +99,7 @@ import { AwaitDesktopDialogComponent } from "./await-desktop-dialog.component"; FormsModule, ReactiveFormsModule, IconButtonModule, + IconModule, ItemModule, JslibModule, LinkModule, diff --git a/apps/web/src/app/auth/emergency-access/accept/accept-emergency.component.html b/apps/web/src/app/auth/emergency-access/accept/accept-emergency.component.html index 3fa795db157..bef7bfe0888 100644 --- a/apps/web/src/app/auth/emergency-access/accept/accept-emergency.component.html +++ b/apps/web/src/app/auth/emergency-access/accept/accept-emergency.component.html @@ -1,12 +1,11 @@

- - {{ "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 cc08b840c30..63bfbf3ef0b 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 @@ -2,12 +2,11 @@

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

diff --git a/apps/web/src/app/auth/settings/account/change-avatar-dialog.component.html b/apps/web/src/app/auth/settings/account/change-avatar-dialog.component.html index 7e8b69a0c48..e9365fa41a4 100644 --- a/apps/web/src/app/auth/settings/account/change-avatar-dialog.component.html +++ b/apps/web/src/app/auth/settings/account/change-avatar-dialog.component.html @@ -1,7 +1,7 @@
- + {{ "loading" | i18n }}

{{ "pickAnAvatarColor" | i18n }}

@@ -31,10 +31,11 @@ class="tw-relative tw-flex tw-size-24 tw-cursor-pointer tw-place-content-center tw-rounded-full tw-border tw-border-solid tw-border-secondary-600 tw-outline tw-outline-0 tw-outline-offset-1 hover:tw-outline-1 hover:tw-outline-primary-300 focus:tw-outline-2 focus:tw-outline-primary-600" [style.background-color]="customColor$ | async" > - + class="!tw-text-muted tw-m-auto tw-text-3xl" + > - - {{ "loading" | i18n }} +
@@ -32,8 +31,8 @@ appStopProp [bitAction]="openChangeAvatar" > - - Customize + + {{ "customize" | i18n }}
@@ -43,7 +42,7 @@ rel="noopener noreferrer" href="https://bitwarden.com/help/claimed-accounts" > - +
@if (fingerprintMaterial && userPublicKey) { diff --git a/apps/web/src/app/auth/settings/emergency-access/emergency-access-add-edit.component.html b/apps/web/src/app/auth/settings/emergency-access/emergency-access-add-edit.component.html index 1c04c03a8d2..0ba4b29690b 100644 --- a/apps/web/src/app/auth/settings/emergency-access/emergency-access-add-edit.component.html +++ b/apps/web/src/app/auth/settings/emergency-access/emergency-access-add-edit.component.html @@ -25,7 +25,7 @@ href="https://bitwarden.com/help/emergency-access/#user-access" slot="end" > - + diff --git a/apps/web/src/app/auth/settings/emergency-access/emergency-access.component.html b/apps/web/src/app/auth/settings/emergency-access/emergency-access.component.html index 70165a94fc3..fe34a63d349 100644 --- a/apps/web/src/app/auth/settings/emergency-access/emergency-access.component.html +++ b/apps/web/src/app/auth/settings/emergency-access/emergency-access.component.html @@ -30,7 +30,7 @@ [bitAction]="invite" [disabled]="!(canAccessPremium$ | async)" > - + {{ "addEmergencyContact" | i18n }} @@ -100,7 +100,7 @@ *ngIf="c.status === emergencyAccessStatusType.Invited" (click)="reinvite(c)" > - + {{ "resendInvitation" | i18n }} @@ -145,12 +145,11 @@

{{ "noTrustedContacts" | i18n }}

- - {{ "loading" | i18n }} +
@@ -223,7 +222,7 @@ *ngIf="c.status === emergencyAccessStatusType.Confirmed" (click)="requestAccess(c)" > - + {{ "requestAccess" | i18n }} @@ -262,12 +261,11 @@

{{ "noGrantedAccess" | i18n }}

- - {{ "loading" | i18n }} +
diff --git a/apps/web/src/app/auth/settings/emergency-access/takeover/emergency-access-takeover-dialog.component.html b/apps/web/src/app/auth/settings/emergency-access/takeover/emergency-access-takeover-dialog.component.html index 2e0a81da976..e5b81671d7f 100644 --- a/apps/web/src/app/auth/settings/emergency-access/takeover/emergency-access-takeover-dialog.component.html +++ b/apps/web/src/app/auth/settings/emergency-access/takeover/emergency-access-takeover-dialog.component.html @@ -7,12 +7,11 @@
@if (initializing) {
- - {{ "loading" | i18n }} +
} @else { diff --git a/apps/web/src/app/auth/settings/emergency-access/takeover/emergency-access-takeover-dialog.component.ts b/apps/web/src/app/auth/settings/emergency-access/takeover/emergency-access-takeover-dialog.component.ts index 743f41537e9..09b6934d2d2 100644 --- a/apps/web/src/app/auth/settings/emergency-access/takeover/emergency-access-takeover-dialog.component.ts +++ b/apps/web/src/app/auth/settings/emergency-access/takeover/emergency-access-takeover-dialog.component.ts @@ -21,6 +21,7 @@ import { DialogModule, DialogRef, DialogService, + IconModule, ToastService, } from "@bitwarden/components"; import { I18nPipe } from "@bitwarden/ui-common"; @@ -59,6 +60,7 @@ export type EmergencyAccessTakeoverDialogResultType = CommonModule, DialogModule, I18nPipe, + IconModule, InputPasswordComponent, ], }) diff --git a/apps/web/src/app/auth/settings/emergency-access/view/emergency-access-view.component.html b/apps/web/src/app/auth/settings/emergency-access/view/emergency-access-view.component.html index 4aaac6aaa52..6c9637ab921 100644 --- a/apps/web/src/app/auth/settings/emergency-access/view/emergency-access-view.component.html +++ b/apps/web/src/app/auth/settings/emergency-access/view/emergency-access-view.component.html @@ -18,22 +18,20 @@ >{{ currentCipher.name }} - - {{ "shared" | i18n }} + [ariaLabel]="'shared' | i18n" + > - - {{ "attachments" | i18n }} + [ariaLabel]="'attachments' | i18n" + >
{{ currentCipher.subTitle }} @@ -43,11 +41,10 @@ - - {{ "loading" | i18n }} +
diff --git a/apps/web/src/app/auth/settings/two-factor/two-factor-setup-authenticator.component.html b/apps/web/src/app/auth/settings/two-factor/two-factor-setup-authenticator.component.html index 1595c0350d0..a2f39ea3c21 100644 --- a/apps/web/src/app/auth/settings/two-factor/two-factor-setup-authenticator.component.html +++ b/apps/web/src/app/auth/settings/two-factor/two-factor-setup-authenticator.component.html @@ -73,7 +73,7 @@

- +

{{ "twoStepAuthenticatorQRCanvasError" | i18n }}

diff --git a/apps/web/src/app/auth/settings/two-factor/two-factor-setup-authenticator.component.ts b/apps/web/src/app/auth/settings/two-factor/two-factor-setup-authenticator.component.ts index d27e8ffecce..0c512b7b99a 100644 --- a/apps/web/src/app/auth/settings/two-factor/two-factor-setup-authenticator.component.ts +++ b/apps/web/src/app/auth/settings/two-factor/two-factor-setup-authenticator.component.ts @@ -29,6 +29,7 @@ import { DialogRef, DialogService, FormFieldModule, + IconModule, SvgModule, InputModule, LinkModule, @@ -63,6 +64,7 @@ declare global { ReactiveFormsModule, DialogModule, FormFieldModule, + IconModule, InputModule, LinkModule, TypographyModule, diff --git a/apps/web/src/app/auth/settings/two-factor/two-factor-setup-webauthn.component.html b/apps/web/src/app/auth/settings/two-factor/two-factor-setup-webauthn.component.html index 8a538cb961c..31ac01fe1da 100644 --- a/apps/web/src/app/auth/settings/two-factor/two-factor-setup-webauthn.component.html +++ b/apps/web/src/app/auth/settings/two-factor/two-factor-setup-webauthn.component.html @@ -17,7 +17,7 @@
  • - + {{ k.name || ("unnamedKey" | i18n) }} @@ -27,12 +27,12 @@ - + > - {{ "remove" | i18n }} @@ -68,19 +68,27 @@ {{ "readKey" | i18n }} - + - + {{ "twoFactorU2fWaiting" | i18n }}... - + {{ "twoFactorU2fClickSave" | i18n }} - + {{ "twoFactorU2fProblemReadingTryAgain" | i18n }} diff --git a/apps/web/src/app/auth/settings/two-factor/two-factor-setup-webauthn.component.ts b/apps/web/src/app/auth/settings/two-factor/two-factor-setup-webauthn.component.ts index 57001acc4d2..22cc6e9d9b3 100644 --- a/apps/web/src/app/auth/settings/two-factor/two-factor-setup-webauthn.component.ts +++ b/apps/web/src/app/auth/settings/two-factor/two-factor-setup-webauthn.component.ts @@ -27,6 +27,7 @@ import { DialogRef, DialogService, FormFieldModule, + IconModule, LinkModule, ToastService, TypographyModule, @@ -56,6 +57,7 @@ interface Key { DialogModule, FormFieldModule, I18nPipe, + IconModule, JslibModule, LinkModule, ReactiveFormsModule, diff --git a/apps/web/src/app/auth/settings/two-factor/two-factor-setup.component.html b/apps/web/src/app/auth/settings/two-factor/two-factor-setup.component.html index 69a0dbf4145..77c410e8ec6 100644 --- a/apps/web/src/app/auth/settings/two-factor/two-factor-setup.component.html +++ b/apps/web/src/app/auth/settings/two-factor/two-factor-setup.component.html @@ -34,12 +34,11 @@

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

    @@ -59,12 +58,11 @@ {{ p.name }} - - {{ "enabled" | i18n }} + diff --git a/apps/web/src/app/auth/settings/webauthn-login-settings/delete-credential-dialog/delete-credential-dialog.component.html b/apps/web/src/app/auth/settings/webauthn-login-settings/delete-credential-dialog/delete-credential-dialog.component.html index 147fc9874dd..910da970ff0 100644 --- a/apps/web/src/app/auth/settings/webauthn-login-settings/delete-credential-dialog/delete-credential-dialog.component.html +++ b/apps/web/src/app/auth/settings/webauthn-login-settings/delete-credential-dialog/delete-credential-dialog.component.html @@ -8,7 +8,11 @@ - + diff --git a/apps/web/src/app/auth/settings/webauthn-login-settings/enable-encryption-dialog/enable-encryption-dialog.component.html b/apps/web/src/app/auth/settings/webauthn-login-settings/enable-encryption-dialog/enable-encryption-dialog.component.html index 3fe6f43a052..85fa35ed4da 100644 --- a/apps/web/src/app/auth/settings/webauthn-login-settings/enable-encryption-dialog/enable-encryption-dialog.component.html +++ b/apps/web/src/app/auth/settings/webauthn-login-settings/enable-encryption-dialog/enable-encryption-dialog.component.html @@ -8,7 +8,11 @@ - + diff --git a/apps/web/src/app/auth/settings/webauthn-login-settings/webauthn-login-settings.component.html b/apps/web/src/app/auth/settings/webauthn-login-settings/webauthn-login-settings.component.html index 2ef177922a9..dd260848f52 100644 --- a/apps/web/src/app/auth/settings/webauthn-login-settings/webauthn-login-settings.component.html +++ b/apps/web/src/app/auth/settings/webauthn-login-settings/webauthn-login-settings.component.html @@ -21,7 +21,7 @@ - +

    @@ -36,7 +36,7 @@ {{ credential.name }} - + {{ "usedForEncryption" | i18n }} @@ -47,7 +47,7 @@ [attr.aria-label]="('enablePasskeyEncryption' | i18n) + ' ' + credential.name" (click)="enableEncryption(credential.id)" > - + {{ "enablePasskeyEncryption" | i18n }} diff --git a/apps/web/src/app/auth/shared/components/user-verification/user-verification.component.html b/apps/web/src/app/auth/shared/components/user-verification/user-verification.component.html index 0770ea4dfe1..1f16fe817e1 100644 --- a/apps/web/src/app/auth/shared/components/user-verification/user-verification.component.html +++ b/apps/web/src/app/auth/shared/components/user-verification/user-verification.component.html @@ -21,7 +21,7 @@ {{ "sendCode" | i18n }} - + {{ "codeSent" | 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 63437352e19..47e0d0f1517 100644 --- a/apps/web/src/app/auth/verify-email-token.component.html +++ b/apps/web/src/app/auth/verify-email-token.component.html @@ -1,6 +1,10 @@

    Bitwarden
    - +
    diff --git a/apps/web/src/app/auth/verify-email-token.component.ts b/apps/web/src/app/auth/verify-email-token.component.ts index fe70f876bc4..f05f9c08b76 100644 --- a/apps/web/src/app/auth/verify-email-token.component.ts +++ b/apps/web/src/app/auth/verify-email-token.component.ts @@ -12,11 +12,14 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { ToastService } from "@bitwarden/components"; +import { SharedModule } from "../shared/shared.module"; + // FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush // eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-verify-email-token", templateUrl: "verify-email-token.component.html", + imports: [SharedModule], }) export class VerifyEmailTokenComponent implements OnInit { constructor( diff --git a/libs/angular/src/auth/device-management/device-management-table.component.html b/libs/angular/src/auth/device-management/device-management-table.component.html index 72187b2a2fc..4c7e0bcb92d 100644 --- a/libs/angular/src/auth/device-management/device-management-table.component.html +++ b/libs/angular/src/auth/device-management/device-management-table.component.html @@ -18,7 +18,7 @@
    - +
    diff --git a/libs/angular/src/auth/device-management/device-management-table.component.ts b/libs/angular/src/auth/device-management/device-management-table.component.ts index 36edf6dd336..3e3555cee13 100644 --- a/libs/angular/src/auth/device-management/device-management-table.component.ts +++ b/libs/angular/src/auth/device-management/device-management-table.component.ts @@ -7,6 +7,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { BadgeModule, ButtonModule, + IconModule, LinkModule, TableDataSource, TableModule, @@ -21,7 +22,15 @@ import { DeviceDisplayData } from "./device-management.component"; standalone: true, selector: "auth-device-management-table", templateUrl: "./device-management-table.component.html", - imports: [BadgeModule, ButtonModule, CommonModule, JslibModule, LinkModule, TableModule], + imports: [ + BadgeModule, + ButtonModule, + CommonModule, + IconModule, + JslibModule, + LinkModule, + TableModule, + ], }) export class DeviceManagementTableComponent implements OnChanges { // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals diff --git a/libs/angular/src/auth/device-management/device-management.component.html b/libs/angular/src/auth/device-management/device-management.component.html index 2a91c2daae2..1b113082254 100644 --- a/libs/angular/src/auth/device-management/device-management.component.html +++ b/libs/angular/src/auth/device-management/device-management.component.html @@ -8,7 +8,7 @@ [bitPopoverTriggerFor]="infoPopover" position="right-start" > - + @@ -23,7 +23,11 @@ @if (initializing) {
    - +
    } @else { diff --git a/libs/angular/src/auth/device-management/device-management.component.ts b/libs/angular/src/auth/device-management/device-management.component.ts index d8f8cc10df4..c697ea44099 100644 --- a/libs/angular/src/auth/device-management/device-management.component.ts +++ b/libs/angular/src/auth/device-management/device-management.component.ts @@ -19,7 +19,7 @@ import { DeviceType, DeviceTypeMetadata } from "@bitwarden/common/enums"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; import { MessageListener } from "@bitwarden/common/platform/messaging"; -import { ButtonModule, DialogService, PopoverModule } from "@bitwarden/components"; +import { ButtonModule, DialogService, IconModule, PopoverModule } from "@bitwarden/components"; import { I18nPipe } from "@bitwarden/ui-common"; import { LoginApprovalDialogComponent } from "../login-approval"; @@ -62,6 +62,7 @@ export interface DeviceDisplayData { DeviceManagementItemGroupComponent, DeviceManagementTableComponent, I18nPipe, + IconModule, PopoverModule, ], }) diff --git a/libs/angular/src/auth/environment-selector/environment-selector.component.html b/libs/angular/src/auth/environment-selector/environment-selector.component.html index 72d7355c399..a1115d94712 100644 --- a/libs/angular/src/auth/environment-selector/environment-selector.component.html +++ b/libs/angular/src/auth/environment-selector/environment-selector.component.html @@ -12,12 +12,12 @@ [attr.aria-pressed]="data.selectedRegion === region ? 'true' : 'false'" (click)="toggle(region.key)" > - + > {{ region.domain }} @@ -41,7 +41,7 @@ {{ data.selectedRegion?.domain || ("selfHostedServer" | i18n) }} - +
    diff --git a/libs/angular/src/auth/environment-selector/environment-selector.component.ts b/libs/angular/src/auth/environment-selector/environment-selector.component.ts index 89366f47b70..79df6a2d992 100644 --- a/libs/angular/src/auth/environment-selector/environment-selector.component.ts +++ b/libs/angular/src/auth/environment-selector/environment-selector.component.ts @@ -13,6 +13,7 @@ import { import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { DialogService, + IconModule, LinkModule, MenuModule, ToastService, @@ -26,7 +27,7 @@ import { I18nPipe } from "@bitwarden/ui-common"; selector: "environment-selector", templateUrl: "environment-selector.component.html", standalone: true, - imports: [CommonModule, I18nPipe, MenuModule, LinkModule, TypographyModule], + imports: [CommonModule, I18nPipe, IconModule, LinkModule, MenuModule, TypographyModule], }) export class EnvironmentSelectorComponent implements OnDestroy { protected ServerEnvironmentType = Region; diff --git a/libs/angular/src/auth/login-approval/login-approval-dialog.component.html b/libs/angular/src/auth/login-approval/login-approval-dialog.component.html index f2850406235..fdaf6584251 100644 --- a/libs/angular/src/auth/login-approval/login-approval-dialog.component.html +++ b/libs/angular/src/auth/login-approval/login-approval-dialog.component.html @@ -4,7 +4,11 @@
    - +
    diff --git a/libs/angular/src/auth/login-approval/login-approval-dialog.component.ts b/libs/angular/src/auth/login-approval/login-approval-dialog.component.ts index 54906047535..36e553aa7d9 100644 --- a/libs/angular/src/auth/login-approval/login-approval-dialog.component.ts +++ b/libs/angular/src/auth/login-approval/login-approval-dialog.component.ts @@ -20,6 +20,7 @@ import { ButtonModule, DialogModule, DialogService, + IconModule, ToastService, } from "@bitwarden/components"; import { LogService } from "@bitwarden/logging"; @@ -35,7 +36,7 @@ export interface LoginApprovalDialogParams { // eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "login-approval-dialog.component.html", - imports: [AsyncActionsModule, ButtonModule, CommonModule, DialogModule, JslibModule], + imports: [AsyncActionsModule, ButtonModule, CommonModule, DialogModule, IconModule, JslibModule], }) export class LoginApprovalDialogComponent implements OnInit, OnDestroy { authRequestId: string; diff --git a/libs/angular/src/auth/password-management/change-password/change-password.component.html b/libs/angular/src/auth/password-management/change-password/change-password.component.html index 7604ffacea7..c147af329ce 100644 --- a/libs/angular/src/auth/password-management/change-password/change-password.component.html +++ b/libs/angular/src/auth/password-management/change-password/change-password.component.html @@ -1,10 +1,9 @@ @if (initializing) { - - {{ "loading" | i18n }} + } @else { - + } @else { @if (userType === SetInitialPasswordUserType.OFFBOARDED_TDE_ORG_USER_UNTRUSTED_DEVICE) { diff --git a/libs/angular/src/auth/password-management/set-initial-password/set-initial-password.component.ts b/libs/angular/src/auth/password-management/set-initial-password/set-initial-password.component.ts index 3cafbdb8ff8..1680bf57720 100644 --- a/libs/angular/src/auth/password-management/set-initial-password/set-initial-password.component.ts +++ b/libs/angular/src/auth/password-management/set-initial-password/set-initial-password.component.ts @@ -37,6 +37,7 @@ import { ButtonModule, CalloutComponent, DialogService, + IconModule, ToastService, } from "@bitwarden/components"; import { KeyService } from "@bitwarden/key-management"; @@ -56,7 +57,14 @@ import { @Component({ standalone: true, templateUrl: "set-initial-password.component.html", - imports: [ButtonModule, CalloutComponent, CommonModule, InputPasswordComponent, I18nPipe], + imports: [ + ButtonModule, + CalloutComponent, + CommonModule, + IconModule, + InputPasswordComponent, + I18nPipe, + ], }) export class SetInitialPasswordComponent implements OnInit { protected inputPasswordFlow = InputPasswordFlow.SetInitialPasswordAuthedUser; diff --git a/libs/auth/src/angular/fingerprint-dialog/fingerprint-dialog.component.html b/libs/auth/src/angular/fingerprint-dialog/fingerprint-dialog.component.html index d66a3a77d93..ed43a32d38c 100644 --- a/libs/auth/src/angular/fingerprint-dialog/fingerprint-dialog.component.html +++ b/libs/auth/src/angular/fingerprint-dialog/fingerprint-dialog.component.html @@ -1,5 +1,5 @@ - + {{ "yourAccountsFingerprint" | i18n }}: @@ -16,7 +16,7 @@ bitDialogClose > {{ "learnMore" | i18n }} - +
    @@ -78,7 +78,7 @@ [buttonType]="ssoRequired ? 'primary' : 'secondary'" (click)="handleSsoClick()" > - + {{ "useSingleSignOn" | i18n }} @@ -114,7 +114,7 @@ buttonType="secondary" (click)="startAuthRequestLogin()" > - + {{ "loginWithDevice" | i18n }}
    diff --git a/libs/auth/src/angular/login/login.component.ts b/libs/auth/src/angular/login/login.component.ts index 8e688f3f830..9957c77ffaf 100644 --- a/libs/auth/src/angular/login/login.component.ts +++ b/libs/auth/src/angular/login/login.component.ts @@ -52,6 +52,7 @@ import { CheckboxModule, FormFieldModule, IconButtonModule, + IconModule, LinkModule, ToastService, TooltipDirective, @@ -79,6 +80,7 @@ export enum LoginUiState { CommonModule, FormFieldModule, IconButtonModule, + IconModule, LinkModule, JslibModule, ReactiveFormsModule, 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 aa6b5c8edc3..031fec5c403 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 @@ -1,5 +1,5 @@
    - +
    (); diff --git a/libs/auth/src/angular/self-hosted-env-config-dialog/self-hosted-env-config-dialog.component.html b/libs/auth/src/angular/self-hosted-env-config-dialog/self-hosted-env-config-dialog.component.html index 92c2f9f2f7a..bf40b15b5da 100644 --- a/libs/auth/src/angular/self-hosted-env-config-dialog/self-hosted-env-config-dialog.component.html +++ b/libs/auth/src/angular/self-hosted-env-config-dialog/self-hosted-env-config-dialog.component.html @@ -16,11 +16,10 @@ @@ -91,7 +90,7 @@ aria-live="assertive" role="alert" > - {{ "selfHostedEnvFormInvalid" | i18n }} + {{ "selfHostedEnvFormInvalid" | i18n }}
    diff --git a/libs/auth/src/angular/self-hosted-env-config-dialog/self-hosted-env-config-dialog.component.ts b/libs/auth/src/angular/self-hosted-env-config-dialog/self-hosted-env-config-dialog.component.ts index 6fb40179afa..6e093a423b3 100644 --- a/libs/auth/src/angular/self-hosted-env-config-dialog/self-hosted-env-config-dialog.component.ts +++ b/libs/auth/src/angular/self-hosted-env-config-dialog/self-hosted-env-config-dialog.component.ts @@ -27,6 +27,7 @@ import { DialogModule, DialogService, FormFieldModule, + IconModule, LinkModule, TypographyModule, } from "@bitwarden/components"; @@ -85,6 +86,7 @@ function onlyHttpsValidator(): ValidatorFn { JslibModule, DialogModule, ButtonModule, + IconModule, LinkModule, TypographyModule, ReactiveFormsModule, diff --git a/libs/auth/src/angular/sso/sso.component.html b/libs/auth/src/angular/sso/sso.component.html index be38f63987e..9ab11b0d094 100644 --- a/libs/auth/src/angular/sso/sso.component.html +++ b/libs/auth/src/angular/sso/sso.component.html @@ -1,6 +1,6 @@
    - + {{ "loading" | i18n }}
    diff --git a/libs/auth/src/angular/sso/sso.component.ts b/libs/auth/src/angular/sso/sso.component.ts index f5167cb84cc..df358e89107 100644 --- a/libs/auth/src/angular/sso/sso.component.ts +++ b/libs/auth/src/angular/sso/sso.component.ts @@ -42,6 +42,7 @@ import { CheckboxModule, FormFieldModule, IconButtonModule, + IconModule, LinkModule, ToastService, } from "@bitwarden/components"; @@ -73,6 +74,7 @@ interface QueryParams { CommonModule, FormFieldModule, IconButtonModule, + IconModule, LinkModule, JslibModule, ReactiveFormsModule, diff --git a/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-webauthn/two-factor-auth-webauthn.component.html b/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-webauthn/two-factor-auth-webauthn.component.html index 6f13b0a1fe2..4c23ab4af5c 100644 --- a/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-webauthn/two-factor-auth-webauthn.component.html +++ b/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-webauthn/two-factor-auth-webauthn.component.html @@ -1,6 +1,6 @@
    - +