From ede4776433a41b1d4678d3402307c4c557fda887 Mon Sep 17 00:00:00 2001 From: Andreas Coroiu Date: Thu, 17 Apr 2025 16:25:35 +0200 Subject: [PATCH 001/499] [PM-18043] Add serialization/deserialization support (#13804) * feat: add support for serialization-PR tweaks * feat: update sdk version --- .../src/platform/ipc/background-communication-backend.ts | 6 +++++- .../src/app/platform/ipc/web-communication-provider.ts | 8 +++++++- package-lock.json | 8 ++++---- package.json | 2 +- 4 files changed, 17 insertions(+), 7 deletions(-) diff --git a/apps/browser/src/platform/ipc/background-communication-backend.ts b/apps/browser/src/platform/ipc/background-communication-backend.ts index 6c5b374dd56..1ebb835fa3b 100644 --- a/apps/browser/src/platform/ipc/background-communication-backend.ts +++ b/apps/browser/src/platform/ipc/background-communication-backend.ts @@ -18,7 +18,11 @@ export class BackgroundCommunicationBackend implements CommunicationBackend { return; } - void this.queue.enqueue({ ...message.message, source: { Web: { id: sender.tab.id } } }); + void this.queue.enqueue( + new IncomingMessage(message.message.payload, message.message.destination, { + Web: { id: sender.tab.id }, + }), + ); }); } diff --git a/apps/web/src/app/platform/ipc/web-communication-provider.ts b/apps/web/src/app/platform/ipc/web-communication-provider.ts index 85353ab77af..787a3c7f3a4 100644 --- a/apps/web/src/app/platform/ipc/web-communication-provider.ts +++ b/apps/web/src/app/platform/ipc/web-communication-provider.ts @@ -19,7 +19,13 @@ export class WebCommunicationProvider implements CommunicationBackend { return; } - await this.queue.enqueue({ ...message.message, source: "BrowserBackground" }); + void this.queue.enqueue( + new IncomingMessage( + message.message.payload, + message.message.destination, + "BrowserBackground", + ), + ); }); } diff --git a/package-lock.json b/package-lock.json index c101444cd8e..65f0c87721a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,7 @@ "@angular/platform-browser": "18.2.13", "@angular/platform-browser-dynamic": "18.2.13", "@angular/router": "18.2.13", - "@bitwarden/sdk-internal": "0.2.0-main.133", + "@bitwarden/sdk-internal": "0.2.0-main.137", "@electron/fuses": "1.8.0", "@emotion/css": "11.13.5", "@koa/multer": "3.0.2", @@ -4700,9 +4700,9 @@ "link": true }, "node_modules/@bitwarden/sdk-internal": { - "version": "0.2.0-main.133", - "resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.133.tgz", - "integrity": "sha512-KzKJGf9cKlcQzfRmqkAwVGBN1kDpcRFkTMm7nrphZSrjfaWJWI1lBEJ0DhnkbMMHJXhQavGyoVk5TIn/Y8ylmw==", + "version": "0.2.0-main.137", + "resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.137.tgz", + "integrity": "sha512-Df0pB5tOEc4WiMjskunTrqHulPzenFv8C61sqsBhHfy80xcf5kU5JyPd4asbf3e4uNS6QGXptd8imp09AuiFEA==", "license": "GPL-3.0" }, "node_modules/@bitwarden/send-ui": { diff --git a/package.json b/package.json index a39004dcc03..c78decb9827 100644 --- a/package.json +++ b/package.json @@ -156,7 +156,7 @@ "@angular/platform-browser": "18.2.13", "@angular/platform-browser-dynamic": "18.2.13", "@angular/router": "18.2.13", - "@bitwarden/sdk-internal": "0.2.0-main.133", + "@bitwarden/sdk-internal": "0.2.0-main.137", "@electron/fuses": "1.8.0", "@emotion/css": "11.13.5", "@koa/multer": "3.0.2", From e0df1ecf0c4b69ebebb34d0a07d54a1f87097918 Mon Sep 17 00:00:00 2001 From: Jonas Hendrickx Date: Thu, 17 Apr 2025 17:33:13 +0200 Subject: [PATCH 002/499] [PM-19180] Calculate sales tax correctly for sponsored plans (#14129) * [PM-19180] Sponsored family org no sales tax because they're free * [PM-19180][DRAFT] Calculate sales tax correctly for sponsored plans with additional storage --------- Co-authored-by: Conner Turnbull <133619638+cturnbull-bitwarden@users.noreply.github.com> --- .../families-for-enterprise-setup.component.ts | 2 ++ .../organizations/organization-plans.component.ts | 14 ++++++++------ .../preview-organization-invoice.request.ts | 3 ++- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/apps/web/src/app/admin-console/organizations/sponsorships/families-for-enterprise-setup.component.ts b/apps/web/src/app/admin-console/organizations/sponsorships/families-for-enterprise-setup.component.ts index fdad689d982..57fe212fa65 100644 --- a/apps/web/src/app/admin-console/organizations/sponsorships/families-for-enterprise-setup.component.ts +++ b/apps/web/src/app/admin-console/organizations/sponsorships/families-for-enterprise-setup.component.ts @@ -43,6 +43,8 @@ export class FamiliesForEnterpriseSetupComponent implements OnInit, OnDestroy { value.plan = PlanType.FamiliesAnnually; value.productTier = ProductTierType.Families; value.acceptingSponsorship = true; + value.planSponsorshipType = PlanSponsorshipType.FamiliesForEnterprise; + // eslint-disable-next-line rxjs-angular/prefer-takeuntil value.onSuccess.subscribe(this.onOrganizationCreateSuccess.bind(this)); } 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 fc7d6793a85..59f8dd34c37 100644 --- a/apps/web/src/app/billing/organizations/organization-plans.component.ts +++ b/apps/web/src/app/billing/organizations/organization-plans.component.ts @@ -34,7 +34,12 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions"; import { TaxServiceAbstraction } from "@bitwarden/common/billing/abstractions/tax.service.abstraction"; -import { PaymentMethodType, PlanType, ProductTierType } from "@bitwarden/common/billing/enums"; +import { + PaymentMethodType, + PlanSponsorshipType, + PlanType, + ProductTierType, +} from "@bitwarden/common/billing/enums"; import { TaxInformation } from "@bitwarden/common/billing/models/domain"; import { ExpandedTaxInfoUpdateRequest } from "@bitwarden/common/billing/models/request/expanded-tax-info-update.request"; import { PreviewOrganizationInvoiceRequest } from "@bitwarden/common/billing/models/request/preview-organization-invoice.request"; @@ -83,6 +88,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { @Input() showFree = true; @Input() showCancel = false; @Input() acceptingSponsorship = false; + @Input() planSponsorshipType?: PlanSponsorshipType; @Input() currentPlan: PlanResponse; selectedFile: File; @@ -682,11 +688,6 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { } private refreshSalesTax(): void { - if (this.formGroup.controls.plan.value == PlanType.Free) { - this.estimatedTax = 0; - return; - } - if (!this.taxComponent.validate()) { return; } @@ -696,6 +697,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { passwordManager: { additionalStorage: this.formGroup.controls.additionalStorage.value, plan: this.formGroup.controls.plan.value, + sponsoredPlan: this.planSponsorshipType, seats: this.formGroup.controls.additionalSeats.value, }, taxInformation: { diff --git a/libs/common/src/billing/models/request/preview-organization-invoice.request.ts b/libs/common/src/billing/models/request/preview-organization-invoice.request.ts index 40d8db03d3b..bfeecb4eb23 100644 --- a/libs/common/src/billing/models/request/preview-organization-invoice.request.ts +++ b/libs/common/src/billing/models/request/preview-organization-invoice.request.ts @@ -1,4 +1,4 @@ -import { PlanType } from "../../enums"; +import { PlanSponsorshipType, PlanType } from "../../enums"; export class PreviewOrganizationInvoiceRequest { organizationId?: string; @@ -21,6 +21,7 @@ export class PreviewOrganizationInvoiceRequest { class PasswordManager { plan: PlanType; + sponsoredPlan?: PlanSponsorshipType; seats: number; additionalStorage: number; From 5af12505f1f97d172d6a7c44a9557b062c406221 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Thu, 17 Apr 2025 16:01:02 -0400 Subject: [PATCH 003/499] Switch userVisibleOnly to `false` (#14202) --- .../notifications/internal/worker-webpush-connection.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/common/src/platform/notifications/internal/worker-webpush-connection.service.ts b/libs/common/src/platform/notifications/internal/worker-webpush-connection.service.ts index 74981b6782f..a1143d14d1d 100644 --- a/libs/common/src/platform/notifications/internal/worker-webpush-connection.service.ts +++ b/libs/common/src/platform/notifications/internal/worker-webpush-connection.service.ts @@ -134,7 +134,7 @@ class MyWebPushConnector implements WebPushConnector { private async pushManagerSubscribe(key: string) { return await this.serviceWorkerRegistration.pushManager.subscribe({ - userVisibleOnly: true, + userVisibleOnly: false, applicationServerKey: key, }); } From 7bf4bae7c6d693c8d438aaa082e28a147be85e1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20=C3=85berg?= Date: Fri, 18 Apr 2025 10:43:22 +0200 Subject: [PATCH 004/499] Revert event-driven .show, in order for bootstrap/hide-to-tray to work (#14181) --- apps/desktop/src/main/window.main.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/apps/desktop/src/main/window.main.ts b/apps/desktop/src/main/window.main.ts index 73231f1f730..1c396b09b21 100644 --- a/apps/desktop/src/main/window.main.ts +++ b/apps/desktop/src/main/window.main.ts @@ -292,11 +292,7 @@ export class WindowMain { this.win.maximize(); } - // Show it later since it might need to be maximized. - // use once event to avoid flash on unstyled content. - this.win.once("ready-to-show", () => { - this.win.show(); - }); + this.win.show(); if (template === "full-app") { // and load the index.html of the app. From d6beca569c4e80bd1657f5e0aca3a9f18ad697c2 Mon Sep 17 00:00:00 2001 From: Vijay Oommen Date: Fri, 18 Apr 2025 08:45:05 -0500 Subject: [PATCH 005/499] [PM-19810] Member Access Report - CSV export fix (#14313) --- .../services/member-access-report.mock.ts | 38 +++++++++++++++++++ .../member-access-report.service.spec.ts | 34 ++++++++++++++++- .../services/member-access-report.service.ts | 20 ++++++++++ 3 files changed, 91 insertions(+), 1 deletion(-) diff --git a/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report.mock.ts b/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report.mock.ts index 9ace555dd2e..b07e4946ca7 100644 --- a/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report.mock.ts +++ b/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report.mock.ts @@ -229,3 +229,41 @@ export const memberAccessReportsMock: MemberAccessResponse[] = [ ], } as MemberAccessResponse, ]; + +export const memberAccessWithoutAccessDetailsReportsMock: MemberAccessResponse[] = [ + { + userName: "Alice Smith", + email: "asmith@email.com", + twoFactorEnabled: true, + accountRecoveryEnabled: true, + groupsCount: 2, + collectionsCount: 4, + totalItemCount: 20, + userGuid: "1234", + usesKeyConnector: false, + accessDetails: [ + { + groupId: "", + collectionId: "c1", + collectionName: new EncString("Collection 1"), + groupName: "Alice Group 1", + itemCount: 10, + readOnly: false, + hidePasswords: false, + manage: false, + } as MemberAccessDetails, + ], + } as MemberAccessResponse, + { + userName: "Robert Brown", + email: "rbrown@email.com", + twoFactorEnabled: false, + accountRecoveryEnabled: false, + groupsCount: 2, + collectionsCount: 4, + totalItemCount: 20, + userGuid: "5678", + usesKeyConnector: false, + accessDetails: [] as MemberAccessDetails[], + } as MemberAccessResponse, +]; diff --git a/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report.service.spec.ts b/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report.service.spec.ts index 7d6beca48ec..e6efac83616 100644 --- a/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report.service.spec.ts +++ b/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report.service.spec.ts @@ -4,7 +4,10 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { OrganizationId } from "@bitwarden/common/types/guid"; import { MemberAccessReportApiService } from "./member-access-report-api.service"; -import { memberAccessReportsMock } from "./member-access-report.mock"; +import { + memberAccessReportsMock, + memberAccessWithoutAccessDetailsReportsMock, +} from "./member-access-report.mock"; import { MemberAccessReportService } from "./member-access-report.service"; describe("ImportService", () => { const mockOrganizationId = "mockOrgId" as OrganizationId; @@ -112,5 +115,34 @@ describe("ImportService", () => { ]), ); }); + + it("should generate user report export items and include users with no access", async () => { + reportApiService.getMemberAccessData.mockImplementation(() => + Promise.resolve(memberAccessWithoutAccessDetailsReportsMock), + ); + const result = + await memberAccessReportService.generateUserReportExportItems(mockOrganizationId); + + expect(result).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + email: "asmith@email.com", + name: "Alice Smith", + twoStepLogin: "memberAccessReportTwoFactorEnabledTrue", + accountRecovery: "memberAccessReportAuthenticationEnabledTrue", + group: "Alice Group 1", + totalItems: "10", + }), + expect.objectContaining({ + email: "rbrown@email.com", + name: "Robert Brown", + twoStepLogin: "memberAccessReportTwoFactorEnabledFalse", + accountRecovery: "memberAccessReportAuthenticationEnabledFalse", + group: "memberAccessReportNoGroup", + totalItems: "0", + }), + ]), + ); + }); }); }); diff --git a/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report.service.ts b/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report.service.ts index b7ff5551e2c..029dce8a404 100644 --- a/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report.service.ts +++ b/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report.service.ts @@ -65,6 +65,26 @@ export class MemberAccessReportService { } const exportItems = memberAccessReports.flatMap((report) => { + // to include users without access details + // which means a user has no groups, collections or items + if (report.accessDetails.length === 0) { + return [ + { + email: report.email, + name: report.userName, + twoStepLogin: report.twoFactorEnabled + ? this.i18nService.t("memberAccessReportTwoFactorEnabledTrue") + : this.i18nService.t("memberAccessReportTwoFactorEnabledFalse"), + accountRecovery: report.accountRecoveryEnabled + ? this.i18nService.t("memberAccessReportAuthenticationEnabledTrue") + : this.i18nService.t("memberAccessReportAuthenticationEnabledFalse"), + group: this.i18nService.t("memberAccessReportNoGroup"), + collection: this.i18nService.t("memberAccessReportNoCollection"), + collectionPermission: this.i18nService.t("memberAccessReportNoCollectionPermission"), + totalItems: "0", + }, + ]; + } const userDetails = report.accessDetails.map((detail) => { const collectionName = collectionNameMap.get(detail.collectionName.encryptedString); return { From 9d16435d0858d40783ff8bc5494f27314cf00a0f Mon Sep 17 00:00:00 2001 From: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com> Date: Fri, 18 Apr 2025 09:52:12 -0400 Subject: [PATCH 006/499] docs(ViewModel): Add JSDocs to view to explain proper use (#14214) --- libs/common/src/models/view/view.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/libs/common/src/models/view/view.ts b/libs/common/src/models/view/view.ts index 1f16b3d5958..2869617dca5 100644 --- a/libs/common/src/models/view/view.ts +++ b/libs/common/src/models/view/view.ts @@ -1 +1,5 @@ +// See https://contributing.bitwarden.com/architecture/clients/data-model/#view for proper use. +// View models represent the decrypted state of a corresponding Domain model. +// They typically match the Domain model but contains a decrypted string for any EncString fields. +// Don't use this to represent arbitrary component view data as that isn't what it is for. export class View {} From e026799071e39219f600c8e1fca9fa024b4be844 Mon Sep 17 00:00:00 2001 From: Jonas Hendrickx Date: Fri, 18 Apr 2025 15:57:27 +0200 Subject: [PATCH 007/499] [PM-13128] Enable Breadcrumb Policies (#13584) * [PM-13128] Enable Breadcrumb Policies * [PM-13128] Enable Breadcrumb Policies * [PM-13128] wip * [PM-13128] wip * [PM-13128] wip * [PM-13128] wip * remove dead code * wip * wip * wip * refactor * Fix for providers * revert to functional auth guard * change prerequisite to info variant * address comment * r * r * r * tests * r * r * fix tests * feedback * fix tests * fix tests * Rename upselling to breadcrumbing * Address feedback * Fix build & tests * Make the guard callback use Observable instead of a promise * Pm 13128 suggestions (#14041) * Rename new enum value * Show the upgrade button when breadcrumbing is enabled * Show mouse pointer when cursor is hovered above badge * Do not make the dialogs overlap * Align badge middle * Gap * Badge should be a `button` instead of `span` * missing button@type --------- Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Co-authored-by: Alex Morask --- .../guards/org-permissions.guard.spec.ts | 30 +++- .../guards/org-permissions.guard.ts | 26 ++- .../organization-layout.component.html | 2 +- .../layouts/organization-layout.component.ts | 15 ++ .../policies/policies.component.html | 15 +- .../policies/policies.component.ts | 61 +++++-- .../policies/policy-edit.component.html | 18 +++ .../policies/policy-edit.component.ts | 33 +++- .../organization-settings-routing.module.ts | 13 +- .../layouts/header/web-header.component.html | 2 +- .../src/services/jslib-services.module.ts | 1 + .../organization-billing.service.ts | 9 ++ .../organization-billing.service.spec.ts | 149 ++++++++++++++++++ .../services/organization-billing.service.ts | 33 +++- 14 files changed, 380 insertions(+), 27 deletions(-) create mode 100644 libs/common/src/billing/services/organization-billing.service.spec.ts diff --git a/apps/web/src/app/admin-console/organizations/guards/org-permissions.guard.spec.ts b/apps/web/src/app/admin-console/organizations/guards/org-permissions.guard.spec.ts index 9afd34ca149..d628e23063f 100644 --- a/apps/web/src/app/admin-console/organizations/guards/org-permissions.guard.spec.ts +++ b/apps/web/src/app/admin-console/organizations/guards/org-permissions.guard.spec.ts @@ -100,20 +100,44 @@ describe("Organization Permissions Guard", () => { it("permits navigation if the user has permissions", async () => { const permissionsCallback = jest.fn(); - permissionsCallback.mockImplementation((_org) => true); + permissionsCallback.mockReturnValue(true); const actual = await TestBed.runInInjectionContext( async () => await organizationPermissionsGuard(permissionsCallback)(route, state), ); - expect(permissionsCallback).toHaveBeenCalledWith(orgFactory({ id: targetOrgId })); + expect(permissionsCallback).toHaveBeenCalledTimes(1); + expect(actual).toBe(true); + }); + + it("handles a Promise returned from the callback", async () => { + const permissionsCallback = jest.fn(); + permissionsCallback.mockReturnValue(Promise.resolve(true)); + + const actual = await TestBed.runInInjectionContext(() => + organizationPermissionsGuard(permissionsCallback)(route, state), + ); + + expect(permissionsCallback).toHaveBeenCalledTimes(1); + expect(actual).toBe(true); + }); + + it("handles an Observable returned from the callback", async () => { + const permissionsCallback = jest.fn(); + permissionsCallback.mockReturnValue(of(true)); + + const actual = await TestBed.runInInjectionContext(() => + organizationPermissionsGuard(permissionsCallback)(route, state), + ); + + expect(permissionsCallback).toHaveBeenCalledTimes(1); expect(actual).toBe(true); }); describe("if the user does not have permissions", () => { it("and there is no Item ID, block navigation", async () => { const permissionsCallback = jest.fn(); - permissionsCallback.mockImplementation((_org) => false); + permissionsCallback.mockReturnValue(false); state = mock({ root: mock({ diff --git a/apps/web/src/app/admin-console/organizations/guards/org-permissions.guard.ts b/apps/web/src/app/admin-console/organizations/guards/org-permissions.guard.ts index d399f9c9c05..6c9090a27b4 100644 --- a/apps/web/src/app/admin-console/organizations/guards/org-permissions.guard.ts +++ b/apps/web/src/app/admin-console/organizations/guards/org-permissions.guard.ts @@ -1,13 +1,13 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { inject } from "@angular/core"; +import { EnvironmentInjector, inject, runInInjectionContext } from "@angular/core"; import { ActivatedRouteSnapshot, CanActivateFn, Router, RouterStateSnapshot, } from "@angular/router"; -import { firstValueFrom, switchMap } from "rxjs"; +import { firstValueFrom, isObservable, Observable, switchMap } from "rxjs"; import { canAccessOrgAdmin, @@ -42,7 +42,9 @@ import { ToastService } from "@bitwarden/components"; * proceeds as expected. */ export function organizationPermissionsGuard( - permissionsCallback?: (organization: Organization) => boolean, + permissionsCallback?: ( + organization: Organization, + ) => boolean | Promise | Observable, ): CanActivateFn { return async (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => { const router = inject(Router); @@ -51,6 +53,7 @@ export function organizationPermissionsGuard( const i18nService = inject(I18nService); const syncService = inject(SyncService); const accountService = inject(AccountService); + const environmentInjector = inject(EnvironmentInjector); // TODO: We need to fix issue once and for all. if ((await syncService.getLastSync()) == null) { @@ -78,7 +81,22 @@ export function organizationPermissionsGuard( return router.createUrlTree(["/"]); } - const hasPermissions = permissionsCallback == null || permissionsCallback(org); + if (permissionsCallback == null) { + // No additional permission checks required, allow navigation + return true; + } + + const callbackResult = runInInjectionContext(environmentInjector, () => + permissionsCallback(org), + ); + + const hasPermissions = isObservable(callbackResult) + ? await firstValueFrom(callbackResult) // handles observables + : await Promise.resolve(callbackResult); // handles promises and boolean values + + if (hasPermissions !== true && hasPermissions !== false) { + throw new Error("Permission callback did not resolve to a boolean."); + } if (!hasPermissions) { // Handle linkable ciphers for organizations the user only has view access to diff --git a/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.html b/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.html index e50c55e83d2..fec790dabcb 100644 --- a/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.html +++ b/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.html @@ -97,7 +97,7 @@ ; protected isBreadcrumbEventLogsEnabled$: Observable; protected showSponsoredFamiliesDropdown$: Observable; + protected canShowPoliciesTab$: Observable; constructor( private route: ActivatedRoute, @@ -79,6 +81,7 @@ export class OrganizationLayoutComponent implements OnInit { protected bannerService: AccountDeprovisioningBannerService, private accountService: AccountService, private freeFamiliesPolicyService: FreeFamiliesPolicyService, + private organizationBillingService: OrganizationBillingServiceAbstraction, ) {} async ngOnInit() { @@ -148,6 +151,18 @@ export class OrganizationLayoutComponent implements OnInit { )) ? "claimedDomains" : "domainVerification"; + + this.canShowPoliciesTab$ = this.organization$.pipe( + switchMap((organization) => + this.organizationBillingService + .isBreadcrumbingPoliciesEnabled$(organization) + .pipe( + map( + (isBreadcrumbingEnabled) => isBreadcrumbingEnabled || organization.canManagePolicies, + ), + ), + ), + ); } canShowVaultTab(organization: Organization): boolean { diff --git a/apps/web/src/app/admin-console/organizations/policies/policies.component.html b/apps/web/src/app/admin-console/organizations/policies/policies.component.html index 24021bb765f..e40b9d80e9e 100644 --- a/apps/web/src/app/admin-console/organizations/policies/policies.component.html +++ b/apps/web/src/app/admin-console/organizations/policies/policies.component.html @@ -1,4 +1,17 @@ - + + @let organization = organization$ | async; + + diff --git a/apps/web/src/app/admin-console/organizations/policies/policies.component.ts b/apps/web/src/app/admin-console/organizations/policies/policies.component.ts index 52cb4da107a..2b86d76d9b1 100644 --- a/apps/web/src/app/admin-console/organizations/policies/policies.component.ts +++ b/apps/web/src/app/admin-console/organizations/policies/policies.component.ts @@ -2,8 +2,8 @@ // @ts-strict-ignore import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; -import { firstValueFrom, lastValueFrom } from "rxjs"; -import { first, map } from "rxjs/operators"; +import { firstValueFrom, lastValueFrom, map, Observable, switchMap } from "rxjs"; +import { first } from "rxjs/operators"; import { getOrganizationById, @@ -14,10 +14,17 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { PolicyResponse } from "@bitwarden/common/admin-console/models/response/policy.response"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing/abstractions"; import { DialogService } from "@bitwarden/components"; +import { + ChangePlanDialogResultType, + openChangePlanDialog, +} from "@bitwarden/web-vault/app/billing/organizations/change-plan-dialog.component"; +import { All } from "@bitwarden/web-vault/app/vault/individual-vault/vault-filter/shared/models/routed-vault-filter.model"; import { PolicyListService } from "../../core/policy-list.service"; import { BasePolicy } from "../policies"; +import { CollectionDialogTabType } from "../shared/components/collection-dialog"; import { PolicyEditComponent, PolicyEditDialogResult } from "./policy-edit.component"; @@ -32,17 +39,19 @@ export class PoliciesComponent implements OnInit { loading = true; organizationId: string; policies: BasePolicy[]; - organization: Organization; + protected organization$: Observable; private orgPolicies: PolicyResponse[]; protected policiesEnabledMap: Map = new Map(); + protected isBreadcrumbingEnabled$: Observable; constructor( private route: ActivatedRoute, - private organizationService: OrganizationService, private accountService: AccountService, + private organizationService: OrganizationService, private policyApiService: PolicyApiServiceAbstraction, private policyListService: PolicyListService, + private organizationBillingService: OrganizationBillingServiceAbstraction, private dialogService: DialogService, ) {} @@ -53,11 +62,9 @@ export class PoliciesComponent implements OnInit { const userId = await firstValueFrom( this.accountService.activeAccount$.pipe(map((a) => a?.id)), ); - this.organization = await firstValueFrom( - this.organizationService - .organizations$(userId) - .pipe(getOrganizationById(this.organizationId)), - ); + this.organization$ = this.organizationService + .organizations$(userId) + .pipe(getOrganizationById(this.organizationId)); this.policies = this.policyListService.getPolicies(); await this.load(); @@ -91,7 +98,11 @@ export class PoliciesComponent implements OnInit { this.orgPolicies.forEach((op) => { this.policiesEnabledMap.set(op.type, op.enabled); }); - + this.isBreadcrumbingEnabled$ = this.organization$.pipe( + switchMap((organization) => + this.organizationBillingService.isBreadcrumbingPoliciesEnabled$(organization), + ), + ); this.loading = false; } @@ -104,8 +115,34 @@ export class PoliciesComponent implements OnInit { }); const result = await lastValueFrom(dialogRef.closed); - if (result === PolicyEditDialogResult.Saved) { - await this.load(); + switch (result) { + case PolicyEditDialogResult.Saved: + await this.load(); + break; + case PolicyEditDialogResult.UpgradePlan: + await this.changePlan(await firstValueFrom(this.organization$)); + break; } } + + protected readonly CollectionDialogTabType = CollectionDialogTabType; + protected readonly All = All; + + protected async changePlan(organization: Organization) { + const reference = openChangePlanDialog(this.dialogService, { + data: { + organizationId: organization.id, + subscription: null, + productTierType: organization.productTierType, + }, + }); + + const result = await lastValueFrom(reference.closed); + + if (result === ChangePlanDialogResultType.Closed) { + return; + } + + await this.load(); + } } diff --git a/apps/web/src/app/admin-console/organizations/policies/policy-edit.component.html b/apps/web/src/app/admin-console/organizations/policies/policy-edit.component.html index 671083a2318..7f33f08f888 100644 --- a/apps/web/src/app/admin-console/organizations/policies/policy-edit.component.html +++ b/apps/web/src/app/admin-console/organizations/policies/policy-edit.component.html @@ -1,5 +1,17 @@
+ + +
+ + + diff --git a/apps/web/src/app/admin-console/organizations/policies/policy-edit.component.ts b/apps/web/src/app/admin-console/organizations/policies/policy-edit.component.ts index f33460e8c16..49f4d15a100 100644 --- a/apps/web/src/app/admin-console/organizations/policies/policy-edit.component.ts +++ b/apps/web/src/app/admin-console/organizations/policies/policy-edit.component.ts @@ -9,12 +9,20 @@ import { ViewContainerRef, } from "@angular/core"; import { FormBuilder } from "@angular/forms"; -import { Observable, map } from "rxjs"; +import { map, Observable, switchMap } from "rxjs"; +import { + getOrganizationById, + OrganizationService, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { PolicyRequest } from "@bitwarden/common/admin-console/models/request/policy.request"; import { PolicyResponse } from "@bitwarden/common/admin-console/models/response/policy.response"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; +import { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing/abstractions"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { DIALOG_DATA, @@ -35,6 +43,7 @@ export type PolicyEditDialogData = { export enum PolicyEditDialogResult { Saved = "saved", + UpgradePlan = "upgrade-plan", } @Component({ selector: "app-policy-edit", @@ -48,22 +57,28 @@ export class PolicyEditComponent implements AfterViewInit { loading = true; enabled = false; saveDisabled$: Observable; - defaultTypes: any[]; policyComponent: BasePolicyComponent; private policyResponse: PolicyResponse; formGroup = this.formBuilder.group({ enabled: [this.enabled], }); + protected organization$: Observable; + protected isBreadcrumbingEnabled$: Observable; + constructor( @Inject(DIALOG_DATA) protected data: PolicyEditDialogData, + private accountService: AccountService, private policyApiService: PolicyApiServiceAbstraction, + private organizationService: OrganizationService, private i18nService: I18nService, private cdr: ChangeDetectorRef, private formBuilder: FormBuilder, private dialogRef: DialogRef, private toastService: ToastService, + private organizationBillingService: OrganizationBillingServiceAbstraction, ) {} + get policy(): BasePolicy { return this.data.policy; } @@ -97,6 +112,16 @@ export class PolicyEditComponent implements AfterViewInit { throw e; } } + this.organization$ = this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => this.organizationService.organizations$(userId)), + getOrganizationById(this.data.organizationId), + ); + this.isBreadcrumbingEnabled$ = this.organization$.pipe( + switchMap((organization) => + this.organizationBillingService.isBreadcrumbingPoliciesEnabled$(organization), + ), + ); } submit = async () => { @@ -119,4 +144,8 @@ export class PolicyEditComponent implements AfterViewInit { static open = (dialogService: DialogService, config: DialogConfig) => { return dialogService.open(PolicyEditComponent, config); }; + + protected upgradePlan(): void { + this.dialogRef.close(PolicyEditDialogResult.UpgradePlan); + } } diff --git a/apps/web/src/app/admin-console/organizations/settings/organization-settings-routing.module.ts b/apps/web/src/app/admin-console/organizations/settings/organization-settings-routing.module.ts index a644086628c..cfec0be531b 100644 --- a/apps/web/src/app/admin-console/organizations/settings/organization-settings-routing.module.ts +++ b/apps/web/src/app/admin-console/organizations/settings/organization-settings-routing.module.ts @@ -1,8 +1,10 @@ -import { NgModule } from "@angular/core"; +import { NgModule, inject } from "@angular/core"; import { RouterModule, Routes } from "@angular/router"; +import { map } from "rxjs"; import { canAccessSettingsTab } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing/abstractions"; import { organizationPermissionsGuard } from "../../organizations/guards/org-permissions.guard"; import { organizationRedirectGuard } from "../../organizations/guards/org-redirect.guard"; @@ -41,7 +43,14 @@ const routes: Routes = [ { path: "policies", component: PoliciesComponent, - canActivate: [organizationPermissionsGuard((org) => org.canManagePolicies)], + canActivate: [ + organizationPermissionsGuard((o: Organization) => { + const organizationBillingService = inject(OrganizationBillingServiceAbstraction); + return organizationBillingService + .isBreadcrumbingPoliciesEnabled$(o) + .pipe(map((isBreadcrumbingEnabled) => o.canManagePolicies || isBreadcrumbingEnabled)); + }), + ], data: { titleId: "policies", }, diff --git a/apps/web/src/app/layouts/header/web-header.component.html b/apps/web/src/app/layouts/header/web-header.component.html index 28d786f2d64..4b0da4ae569 100644 --- a/apps/web/src/app/layouts/header/web-header.component.html +++ b/apps/web/src/app/layouts/header/web-header.component.html @@ -12,7 +12,7 @@

diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 3cce9b5357e..8e2b3409593 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -1255,6 +1255,7 @@ const safeProviders: SafeProvider[] = [ I18nServiceAbstraction, OrganizationApiServiceAbstraction, SyncService, + ConfigService, ], }), safeProvider({ diff --git a/libs/common/src/billing/abstractions/organization-billing.service.ts b/libs/common/src/billing/abstractions/organization-billing.service.ts index 69309014fac..8024a120b0a 100644 --- a/libs/common/src/billing/abstractions/organization-billing.service.ts +++ b/libs/common/src/billing/abstractions/organization-billing.service.ts @@ -1,5 +1,8 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +import { Observable } from "rxjs"; + +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { OrganizationResponse } from "../../admin-console/models/response/organization.response"; import { InitiationPath } from "../../models/request/reference-event.request"; @@ -59,4 +62,10 @@ export abstract class OrganizationBillingServiceAbstraction { organizationId: string, subscription: SubscriptionInformation, ) => Promise; + + /** + * Determines if breadcrumbing policies is enabled for the organizations meeting certain criteria. + * @param organization + */ + abstract isBreadcrumbingPoliciesEnabled$(organization: Organization): Observable; } diff --git a/libs/common/src/billing/services/organization-billing.service.spec.ts b/libs/common/src/billing/services/organization-billing.service.spec.ts new file mode 100644 index 00000000000..7b194dff637 --- /dev/null +++ b/libs/common/src/billing/services/organization-billing.service.spec.ts @@ -0,0 +1,149 @@ +import { mock } from "jest-mock-extended"; +import { firstValueFrom, of } from "rxjs"; + +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { OrganizationApiServiceAbstraction as OrganizationApiService } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions"; +import { ProductTierType } from "@bitwarden/common/billing/enums"; +import { OrganizationBillingService } from "@bitwarden/common/billing/services/organization-billing.service"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +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 { SyncService } from "@bitwarden/common/platform/sync"; +import { KeyService } from "@bitwarden/key-management"; + +describe("BillingAccountProfileStateService", () => { + let apiService: jest.Mocked; + let billingApiService: jest.Mocked; + let keyService: jest.Mocked; + let encryptService: jest.Mocked; + let i18nService: jest.Mocked; + let organizationApiService: jest.Mocked; + let syncService: jest.Mocked; + let configService: jest.Mocked; + + let sut: OrganizationBillingService; + + beforeEach(() => { + apiService = mock(); + billingApiService = mock(); + keyService = mock(); + encryptService = mock(); + i18nService = mock(); + organizationApiService = mock(); + syncService = mock(); + configService = mock(); + + sut = new OrganizationBillingService( + apiService, + billingApiService, + keyService, + encryptService, + i18nService, + organizationApiService, + syncService, + configService, + ); + }); + + afterEach(() => { + return jest.resetAllMocks(); + }); + + describe("isBreadcrumbingPoliciesEnabled", () => { + it("returns false when feature flag is disabled", async () => { + configService.getFeatureFlag$.mockReturnValue(of(false)); + const org = { + isProviderUser: false, + canEditSubscription: true, + productTierType: ProductTierType.Teams, + } as Organization; + + const actual = await firstValueFrom(sut.isBreadcrumbingPoliciesEnabled$(org)); + expect(actual).toBe(false); + expect(configService.getFeatureFlag$).toHaveBeenCalledWith( + FeatureFlag.PM12276_BreadcrumbEventLogs, + ); + }); + + it("returns false when organization belongs to a provider", async () => { + configService.getFeatureFlag$.mockReturnValue(of(true)); + const org = { + isProviderUser: true, + canEditSubscription: true, + productTierType: ProductTierType.Teams, + } as Organization; + + const actual = await firstValueFrom(sut.isBreadcrumbingPoliciesEnabled$(org)); + expect(actual).toBe(false); + }); + + it("returns false when cannot edit subscription", async () => { + configService.getFeatureFlag$.mockReturnValue(of(true)); + const org = { + isProviderUser: false, + canEditSubscription: false, + productTierType: ProductTierType.Teams, + } as Organization; + + const actual = await firstValueFrom(sut.isBreadcrumbingPoliciesEnabled$(org)); + expect(actual).toBe(false); + }); + + it.each([ + ["Teams", ProductTierType.Teams], + ["TeamsStarter", ProductTierType.TeamsStarter], + ])("returns true when all conditions are met with %s tier", async (_, productTierType) => { + configService.getFeatureFlag$.mockReturnValue(of(true)); + const org = { + isProviderUser: false, + canEditSubscription: true, + productTierType: productTierType, + } as Organization; + + const actual = await firstValueFrom(sut.isBreadcrumbingPoliciesEnabled$(org)); + expect(actual).toBe(true); + expect(configService.getFeatureFlag$).toHaveBeenCalledWith( + FeatureFlag.PM12276_BreadcrumbEventLogs, + ); + }); + + it("returns false when product tier is not supported", async () => { + configService.getFeatureFlag$.mockReturnValue(of(true)); + const org = { + isProviderUser: false, + canEditSubscription: true, + productTierType: ProductTierType.Enterprise, + } as Organization; + + const actual = await firstValueFrom(sut.isBreadcrumbingPoliciesEnabled$(org)); + expect(actual).toBe(false); + }); + + it("handles all conditions false correctly", async () => { + configService.getFeatureFlag$.mockReturnValue(of(false)); + const org = { + isProviderUser: true, + canEditSubscription: false, + productTierType: ProductTierType.Free, + } as Organization; + + const actual = await firstValueFrom(sut.isBreadcrumbingPoliciesEnabled$(org)); + expect(actual).toBe(false); + }); + + it("verifies feature flag is only called once", async () => { + configService.getFeatureFlag$.mockReturnValue(of(false)); + const org = { + isProviderUser: false, + canEditSubscription: true, + productTierType: ProductTierType.Teams, + } as Organization; + + await firstValueFrom(sut.isBreadcrumbingPoliciesEnabled$(org)); + expect(configService.getFeatureFlag$).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/libs/common/src/billing/services/organization-billing.service.ts b/libs/common/src/billing/services/organization-billing.service.ts index 83efbf0a30c..6622cdcdce3 100644 --- a/libs/common/src/billing/services/organization-billing.service.ts +++ b/libs/common/src/billing/services/organization-billing.service.ts @@ -1,5 +1,10 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +import { Observable, of, switchMap } from "rxjs"; + +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { KeyService } from "@bitwarden/key-management"; import { ApiService } from "../../abstractions/api.service"; @@ -20,7 +25,7 @@ import { PlanInformation, SubscriptionInformation, } from "../abstractions"; -import { PlanType } from "../enums"; +import { PlanType, ProductTierType } from "../enums"; import { OrganizationNoPaymentMethodCreateRequest } from "../models/request/organization-no-payment-method-create-request"; import { PaymentSourceResponse } from "../models/response/payment-source.response"; @@ -40,6 +45,7 @@ export class OrganizationBillingService implements OrganizationBillingServiceAbs private i18nService: I18nService, private organizationApiService: OrganizationApiService, private syncService: SyncService, + private configService: ConfigService, ) {} async getPaymentSource(organizationId: string): Promise { @@ -220,4 +226,29 @@ export class OrganizationBillingService implements OrganizationBillingServiceAbs this.setPaymentInformation(request, subscription.payment); await this.billingApiService.restartSubscription(organizationId, request); } + + isBreadcrumbingPoliciesEnabled$(organization: Organization): Observable { + if (organization === null || organization === undefined) { + return of(false); + } + + return this.configService.getFeatureFlag$(FeatureFlag.PM12276_BreadcrumbEventLogs).pipe( + switchMap((featureFlagEnabled) => { + if (!featureFlagEnabled) { + return of(false); + } + + if (organization.isProviderUser || !organization.canEditSubscription) { + return of(false); + } + + const supportedProducts = [ProductTierType.Teams, ProductTierType.TeamsStarter]; + const isSupportedProduct = supportedProducts.some( + (product) => product === organization.productTierType, + ); + + return of(isSupportedProduct); + }), + ); + } } From cbab354c0eaecc461c33f6d616fcc212b519bca2 Mon Sep 17 00:00:00 2001 From: Bryan Cunningham Date: Fri, 18 Apr 2025 10:38:19 -0400 Subject: [PATCH 008/499] Hide bit-icon component from screen readers by default (#14295) * adds aria-hidden to bit-icon when no aria-label provided * add ariaLabel to logo svg usages * add ariaLabel documentation * default ariaLable value to undefined * add logo label to translations * adds i18n pipe to component * Add binding to example docs --- apps/browser/src/_locales/en/messages.json | 3 +++ .../extension-anon-layout-wrapper.component.html | 7 ++++++- .../extension-anon-layout-wrapper.component.ts | 2 ++ .../accept-family-sponsorship.component.html | 3 ++- apps/web/src/locales/en/messages.json | 3 +++ .../manage/accept-provider.component.html | 6 +++++- .../providers/setup/setup-provider.component.html | 6 +++++- .../setup/setup-business-unit.component.html | 6 +++++- .../angular/anon-layout/anon-layout.component.html | 2 +- libs/components/src/icon/icon.component.ts | 14 +++++++++----- libs/components/src/icon/icon.mdx | 10 ++++++++++ libs/components/src/icon/icon.stories.ts | 4 ++++ 12 files changed, 55 insertions(+), 11 deletions(-) diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 87b94650b51..b0f3fb5d19e 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -2,6 +2,9 @@ "appName": { "message": "Bitwarden" }, + "appLogoLabel": { + "message": "Bitwarden logo" + }, "extName": { "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" diff --git a/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.html b/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.html index 54cb5203a87..bd2886dacf0 100644 --- a/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.html +++ b/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.html @@ -5,7 +5,12 @@ [showBackButton]="showBackButton" [pageTitle]="''" > - + diff --git a/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.ts b/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.ts index 51dbb6503d7..d6cccf31bb4 100644 --- a/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.ts +++ b/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.ts @@ -12,6 +12,7 @@ import { } from "@bitwarden/auth/angular"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { Icon, IconModule, Translation } from "@bitwarden/components"; +import { I18nPipe } from "@bitwarden/ui-common"; import { PopOutComponent } from "../../../platform/popup/components/pop-out.component"; import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component"; @@ -36,6 +37,7 @@ export interface ExtensionAnonLayoutWrapperData extends AnonLayoutWrapperData { AnonLayoutComponent, CommonModule, CurrentAccountComponent, + I18nPipe, IconModule, PopOutComponent, PopupPageComponent, diff --git a/apps/web/src/app/admin-console/organizations/sponsorships/accept-family-sponsorship.component.html b/apps/web/src/app/admin-console/organizations/sponsorships/accept-family-sponsorship.component.html index fdbb6dbba91..ca1264829b9 100644 --- a/apps/web/src/app/admin-console/organizations/sponsorships/accept-family-sponsorship.component.html +++ b/apps/web/src/app/admin-console/organizations/sponsorships/accept-family-sponsorship.component.html @@ -1,6 +1,7 @@
- + +
- +

- +

- +

- +

NOTE: SVG icons are treated as decorative by default and will be `aria-hidden` unless an + > `ariaLabel` is explicitly provided to the `` component + ```html ``` + With `ariaLabel` + + ```html + + ``` + 8. **Ensure your SVG renders properly** according to Figma in both light and dark modes on a client which supports multiple style modes. diff --git a/libs/components/src/icon/icon.stories.ts b/libs/components/src/icon/icon.stories.ts index 53454567b7f..7892bdd3ec1 100644 --- a/libs/components/src/icon/icon.stories.ts +++ b/libs/components/src/icon/icon.stories.ts @@ -26,5 +26,9 @@ export const Default: Story = { mapping: GenericIcons, control: { type: "select" }, }, + ariaLabel: { + control: "text", + description: "the text used by a screen reader to describe the icon", + }, }, }; From 4da03821cee44184c8567f93131818340a50a5fd Mon Sep 17 00:00:00 2001 From: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com> Date: Fri, 18 Apr 2025 17:23:52 +0200 Subject: [PATCH 009/499] move key connector components to KM team ownership (#14008) --- .../key-connector}/remove-password.component.html | 0 .../key-connector}/remove-password.component.ts | 2 +- apps/browser/src/popup/app-routing.module.ts | 2 +- apps/browser/src/popup/app.module.ts | 2 +- apps/cli/src/auth/commands/unlock.command.ts | 2 +- .../convert-to-key-connector.command.ts | 0 apps/desktop/src/app/app-routing.module.ts | 2 +- apps/desktop/src/app/app.module.ts | 2 +- .../key-connector}/remove-password.component.html | 0 .../key-connector}/remove-password.component.ts | 2 +- .../key-connector}/remove-password.component.html | 0 .../key-connector}/remove-password.component.ts | 2 +- apps/web/src/app/oss-routing.module.ts | 2 +- apps/web/src/app/shared/loose-components.module.ts | 2 +- libs/key-management-ui/src/index.ts | 1 + .../src/key-connector}/remove-password.component.ts | 0 16 files changed, 11 insertions(+), 10 deletions(-) rename apps/browser/src/{auth/popup => key-management/key-connector}/remove-password.component.html (100%) rename apps/browser/src/{auth/popup => key-management/key-connector}/remove-password.component.ts (80%) rename apps/cli/src/{commands => key-management}/convert-to-key-connector.command.ts (100%) rename apps/desktop/src/{auth => key-management/key-connector}/remove-password.component.html (100%) rename apps/desktop/src/{auth => key-management/key-connector}/remove-password.component.ts (80%) rename apps/web/src/app/{auth => key-management/key-connector}/remove-password.component.html (100%) rename apps/web/src/app/{auth => key-management/key-connector}/remove-password.component.ts (80%) rename libs/{angular/src/auth/components => key-management-ui/src/key-connector}/remove-password.component.ts (100%) diff --git a/apps/browser/src/auth/popup/remove-password.component.html b/apps/browser/src/key-management/key-connector/remove-password.component.html similarity index 100% rename from apps/browser/src/auth/popup/remove-password.component.html rename to apps/browser/src/key-management/key-connector/remove-password.component.html diff --git a/apps/browser/src/auth/popup/remove-password.component.ts b/apps/browser/src/key-management/key-connector/remove-password.component.ts similarity index 80% rename from apps/browser/src/auth/popup/remove-password.component.ts rename to apps/browser/src/key-management/key-connector/remove-password.component.ts index 5272a3082a2..3ca9d3a5669 100644 --- a/apps/browser/src/auth/popup/remove-password.component.ts +++ b/apps/browser/src/key-management/key-connector/remove-password.component.ts @@ -1,6 +1,6 @@ import { Component } from "@angular/core"; -import { RemovePasswordComponent as BaseRemovePasswordComponent } from "@bitwarden/angular/auth/components/remove-password.component"; +import { RemovePasswordComponent as BaseRemovePasswordComponent } from "@bitwarden/key-management-ui"; @Component({ selector: "app-remove-password", diff --git a/apps/browser/src/popup/app-routing.module.ts b/apps/browser/src/popup/app-routing.module.ts index 21ac4c19700..45955506b91 100644 --- a/apps/browser/src/popup/app-routing.module.ts +++ b/apps/browser/src/popup/app-routing.module.ts @@ -55,7 +55,6 @@ import { ExtensionAnonLayoutWrapperComponent, ExtensionAnonLayoutWrapperData, } from "../auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component"; -import { RemovePasswordComponent } from "../auth/popup/remove-password.component"; import { SetPasswordComponent } from "../auth/popup/set-password.component"; import { AccountSecurityComponent } from "../auth/popup/settings/account-security.component"; import { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password.component"; @@ -65,6 +64,7 @@ import { BlockedDomainsComponent } from "../autofill/popup/settings/blocked-doma import { ExcludedDomainsComponent } from "../autofill/popup/settings/excluded-domains.component"; import { NotificationsSettingsComponent } from "../autofill/popup/settings/notifications.component"; import { PremiumV2Component } from "../billing/popup/settings/premium-v2.component"; +import { RemovePasswordComponent } from "../key-management/key-connector/remove-password.component"; import BrowserPopupUtils from "../platform/popup/browser-popup-utils"; import { popupRouterCacheGuard } from "../platform/popup/view-cache/popup-router-cache.service"; import { CredentialGeneratorHistoryComponent } from "../tools/popup/generator/credential-generator-history.component"; diff --git a/apps/browser/src/popup/app.module.ts b/apps/browser/src/popup/app.module.ts index 0d392afa63b..8bea41da4d6 100644 --- a/apps/browser/src/popup/app.module.ts +++ b/apps/browser/src/popup/app.module.ts @@ -25,13 +25,13 @@ import { import { AccountComponent } from "../auth/popup/account-switching/account.component"; import { CurrentAccountComponent } from "../auth/popup/account-switching/current-account.component"; import { ExtensionAnonLayoutWrapperComponent } from "../auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component"; -import { RemovePasswordComponent } from "../auth/popup/remove-password.component"; import { SetPasswordComponent } from "../auth/popup/set-password.component"; import { AccountSecurityComponent } from "../auth/popup/settings/account-security.component"; import { VaultTimeoutInputComponent } from "../auth/popup/settings/vault-timeout-input.component"; import { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password.component"; import { AutofillComponent } from "../autofill/popup/settings/autofill.component"; import { NotificationsSettingsComponent } from "../autofill/popup/settings/notifications.component"; +import { RemovePasswordComponent } from "../key-management/key-connector/remove-password.component"; import { PopOutComponent } from "../platform/popup/components/pop-out.component"; import { HeaderComponent } from "../platform/popup/header.component"; import { PopupFooterComponent } from "../platform/popup/layout/popup-footer.component"; diff --git a/apps/cli/src/auth/commands/unlock.command.ts b/apps/cli/src/auth/commands/unlock.command.ts index 6d524759dd6..d10c3f38ebd 100644 --- a/apps/cli/src/auth/commands/unlock.command.ts +++ b/apps/cli/src/auth/commands/unlock.command.ts @@ -17,7 +17,7 @@ import { MasterKey } from "@bitwarden/common/types/key"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { KeyService } from "@bitwarden/key-management"; -import { ConvertToKeyConnectorCommand } from "../../commands/convert-to-key-connector.command"; +import { ConvertToKeyConnectorCommand } from "../../key-management/convert-to-key-connector.command"; import { Response } from "../../models/response"; import { MessageResponse } from "../../models/response/message.response"; import { CliUtils } from "../../utils"; diff --git a/apps/cli/src/commands/convert-to-key-connector.command.ts b/apps/cli/src/key-management/convert-to-key-connector.command.ts similarity index 100% rename from apps/cli/src/commands/convert-to-key-connector.command.ts rename to apps/cli/src/key-management/convert-to-key-connector.command.ts diff --git a/apps/desktop/src/app/app-routing.module.ts b/apps/desktop/src/app/app-routing.module.ts index cd5064a87e4..0c6bc730c2c 100644 --- a/apps/desktop/src/app/app-routing.module.ts +++ b/apps/desktop/src/app/app-routing.module.ts @@ -50,9 +50,9 @@ import { import { AccessibilityCookieComponent } from "../auth/accessibility-cookie.component"; import { maxAccountsGuardFn } from "../auth/guards/max-accounts.guard"; -import { RemovePasswordComponent } from "../auth/remove-password.component"; import { SetPasswordComponent } from "../auth/set-password.component"; import { UpdateTempPasswordComponent } from "../auth/update-temp-password.component"; +import { RemovePasswordComponent } from "../key-management/key-connector/remove-password.component"; import { VaultComponent } from "../vault/app/vault/vault.component"; import { Fido2PlaceholderComponent } from "./components/fido2placeholder.component"; diff --git a/apps/desktop/src/app/app.module.ts b/apps/desktop/src/app/app.module.ts index fdc25ed642e..b892324a979 100644 --- a/apps/desktop/src/app/app.module.ts +++ b/apps/desktop/src/app/app.module.ts @@ -13,11 +13,11 @@ import { DecryptionFailureDialogComponent } from "@bitwarden/vault"; import { AccessibilityCookieComponent } from "../auth/accessibility-cookie.component"; import { DeleteAccountComponent } from "../auth/delete-account.component"; import { LoginModule } from "../auth/login/login.module"; -import { RemovePasswordComponent } from "../auth/remove-password.component"; import { SetPasswordComponent } from "../auth/set-password.component"; import { UpdateTempPasswordComponent } from "../auth/update-temp-password.component"; import { SshAgentService } from "../autofill/services/ssh-agent.service"; import { PremiumComponent } from "../billing/app/accounts/premium.component"; +import { RemovePasswordComponent } from "../key-management/key-connector/remove-password.component"; import { AddEditCustomFieldsComponent } from "../vault/app/vault/add-edit-custom-fields.component"; import { AddEditComponent } from "../vault/app/vault/add-edit.component"; import { AttachmentsComponent } from "../vault/app/vault/attachments.component"; diff --git a/apps/desktop/src/auth/remove-password.component.html b/apps/desktop/src/key-management/key-connector/remove-password.component.html similarity index 100% rename from apps/desktop/src/auth/remove-password.component.html rename to apps/desktop/src/key-management/key-connector/remove-password.component.html diff --git a/apps/desktop/src/auth/remove-password.component.ts b/apps/desktop/src/key-management/key-connector/remove-password.component.ts similarity index 80% rename from apps/desktop/src/auth/remove-password.component.ts rename to apps/desktop/src/key-management/key-connector/remove-password.component.ts index 5272a3082a2..3ca9d3a5669 100644 --- a/apps/desktop/src/auth/remove-password.component.ts +++ b/apps/desktop/src/key-management/key-connector/remove-password.component.ts @@ -1,6 +1,6 @@ import { Component } from "@angular/core"; -import { RemovePasswordComponent as BaseRemovePasswordComponent } from "@bitwarden/angular/auth/components/remove-password.component"; +import { RemovePasswordComponent as BaseRemovePasswordComponent } from "@bitwarden/key-management-ui"; @Component({ selector: "app-remove-password", diff --git a/apps/web/src/app/auth/remove-password.component.html b/apps/web/src/app/key-management/key-connector/remove-password.component.html similarity index 100% rename from apps/web/src/app/auth/remove-password.component.html rename to apps/web/src/app/key-management/key-connector/remove-password.component.html diff --git a/apps/web/src/app/auth/remove-password.component.ts b/apps/web/src/app/key-management/key-connector/remove-password.component.ts similarity index 80% rename from apps/web/src/app/auth/remove-password.component.ts rename to apps/web/src/app/key-management/key-connector/remove-password.component.ts index 5272a3082a2..3ca9d3a5669 100644 --- a/apps/web/src/app/auth/remove-password.component.ts +++ b/apps/web/src/app/key-management/key-connector/remove-password.component.ts @@ -1,6 +1,6 @@ import { Component } from "@angular/core"; -import { RemovePasswordComponent as BaseRemovePasswordComponent } from "@bitwarden/angular/auth/components/remove-password.component"; +import { RemovePasswordComponent as BaseRemovePasswordComponent } from "@bitwarden/key-management-ui"; @Component({ selector: "app-remove-password", diff --git a/apps/web/src/app/oss-routing.module.ts b/apps/web/src/app/oss-routing.module.ts index 0334519516a..fc8356505f5 100644 --- a/apps/web/src/app/oss-routing.module.ts +++ b/apps/web/src/app/oss-routing.module.ts @@ -58,7 +58,6 @@ import { LoginViaWebAuthnComponent } from "./auth/login/login-via-webauthn/login import { AcceptOrganizationComponent } from "./auth/organization-invite/accept-organization.component"; import { RecoverDeleteComponent } from "./auth/recover-delete.component"; import { RecoverTwoFactorComponent } from "./auth/recover-two-factor.component"; -import { RemovePasswordComponent } from "./auth/remove-password.component"; import { SetPasswordComponent } from "./auth/set-password.component"; import { AccountComponent } from "./auth/settings/account/account.component"; import { EmergencyAccessComponent } from "./auth/settings/emergency-access/emergency-access.component"; @@ -73,6 +72,7 @@ import { CompleteTrialInitiationComponent } from "./billing/trial-initiation/com import { freeTrialTextResolver } from "./billing/trial-initiation/complete-trial-initiation/resolver/free-trial-text.resolver"; import { EnvironmentSelectorComponent } from "./components/environment-selector/environment-selector.component"; import { RouteDataProperties } from "./core"; +import { RemovePasswordComponent } from "./key-management/key-connector/remove-password.component"; import { FrontendLayoutComponent } from "./layouts/frontend-layout.component"; import { UserLayoutComponent } from "./layouts/user-layout.component"; import { RequestSMAccessComponent } from "./secrets-manager/secrets-manager-landing/request-sm-access.component"; diff --git a/apps/web/src/app/shared/loose-components.module.ts b/apps/web/src/app/shared/loose-components.module.ts index 469ebe457d0..eb63b9f798c 100644 --- a/apps/web/src/app/shared/loose-components.module.ts +++ b/apps/web/src/app/shared/loose-components.module.ts @@ -15,7 +15,6 @@ import { VerifyRecoverDeleteOrgComponent } from "../admin-console/organizations/ import { AcceptFamilySponsorshipComponent } from "../admin-console/organizations/sponsorships/accept-family-sponsorship.component"; import { RecoverDeleteComponent } from "../auth/recover-delete.component"; import { RecoverTwoFactorComponent } from "../auth/recover-two-factor.component"; -import { RemovePasswordComponent } from "../auth/remove-password.component"; import { SetPasswordComponent } from "../auth/set-password.component"; import { AccountComponent } from "../auth/settings/account/account.component"; import { ChangeAvatarDialogComponent } from "../auth/settings/account/change-avatar-dialog.component"; @@ -42,6 +41,7 @@ import { SponsoredFamiliesComponent } from "../billing/settings/sponsored-famili import { SponsoringOrgRowComponent } from "../billing/settings/sponsoring-org-row.component"; import { DynamicAvatarComponent } from "../components/dynamic-avatar.component"; import { SelectableAvatarComponent } from "../components/selectable-avatar.component"; +import { RemovePasswordComponent } from "../key-management/key-connector/remove-password.component"; import { FrontendLayoutComponent } from "../layouts/frontend-layout.component"; import { HeaderModule } from "../layouts/header/header.module"; import { ProductSwitcherModule } from "../layouts/product-switcher/product-switcher.module"; diff --git a/libs/key-management-ui/src/index.ts b/libs/key-management-ui/src/index.ts index 2f98538caad..b330e390d36 100644 --- a/libs/key-management-ui/src/index.ts +++ b/libs/key-management-ui/src/index.ts @@ -7,3 +7,4 @@ export { LockComponentService, UnlockOptions } from "./lock/services/lock-compon export { KeyRotationTrustInfoComponent } from "./key-rotation/key-rotation-trust-info.component"; export { AccountRecoveryTrustComponent } from "./trust/account-recovery-trust.component"; export { EmergencyAccessTrustComponent } from "./trust/emergency-access-trust.component"; +export { RemovePasswordComponent } from "./key-connector/remove-password.component"; diff --git a/libs/angular/src/auth/components/remove-password.component.ts b/libs/key-management-ui/src/key-connector/remove-password.component.ts similarity index 100% rename from libs/angular/src/auth/components/remove-password.component.ts rename to libs/key-management-ui/src/key-connector/remove-password.component.ts From da57b944ae77e29c5032af5f27c35a5eae2f6644 Mon Sep 17 00:00:00 2001 From: Todd Martin <106564991+trmartin4@users.noreply.github.com> Date: Fri, 18 Apr 2025 13:47:04 -0400 Subject: [PATCH 010/499] chore(deps): Remove Platform from owning Cargo.lock --- .github/CODEOWNERS | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 2f402b15dd5..de28b210887 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -8,7 +8,8 @@ apps/desktop/desktop_native @bitwarden/team-platform-dev apps/desktop/desktop_native/objc/src/native/autofill @bitwarden/team-autofill-dev apps/desktop/desktop_native/core/src/autofill @bitwarden/team-autofill-dev -## No ownership for Cargo.toml to allow dependency updates +## No ownership fo Cargo.lock and Cargo.toml to allow dependency updates +apps/desktop/desktop_native/Cargo.lock apps/desktop/desktop_native/Cargo.toml ## Auth team files ## From a82996526279bc683a31c6c944810b37a7836fef Mon Sep 17 00:00:00 2001 From: Vijay Oommen Date: Fri, 18 Apr 2025 13:05:58 -0500 Subject: [PATCH 011/499] [PM-20386] valuesChanges returns a string (#14338) --- .../vault-export-ui/src/components/export.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.ts b/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.ts index 4e9b4175838..71599c19ae0 100644 --- a/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.ts +++ b/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.ts @@ -220,7 +220,7 @@ export class ExportComponent implements OnInit, OnDestroy, AfterViewInit { this.exportForm.controls.vaultSelector.valueChanges .pipe(takeUntil(this.destroy$)) - .subscribe(([value]) => { + .subscribe((value) => { this.organizationId = value !== "myVault" ? value : undefined; this.formatOptions = this.formatOptions.filter((option) => option.value !== "zip"); From f86a5c2b6ea4bf18194f3c5875c8c1e63ea3f36f Mon Sep 17 00:00:00 2001 From: Chase Nelson Date: Fri, 18 Apr 2025 14:55:23 -0400 Subject: [PATCH 012/499] [PM-19798] [PM-18807] Fix base64 encoding/decoding with special characters (#14089) * Refactor base64 encoding/decoding to use BufferLib * Add tests for base64 encoding and decoding functions --------- Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com> --- libs/common/src/platform/misc/utils.spec.ts | 69 +++++++++++++++++++++ libs/common/src/platform/misc/utils.ts | 4 +- 2 files changed, 71 insertions(+), 2 deletions(-) diff --git a/libs/common/src/platform/misc/utils.spec.ts b/libs/common/src/platform/misc/utils.spec.ts index 964a2a19413..818138863fb 100644 --- a/libs/common/src/platform/misc/utils.spec.ts +++ b/libs/common/src/platform/misc/utils.spec.ts @@ -706,4 +706,73 @@ describe("Utils Service", () => { }); }); }); + + describe("fromUtf8ToB64(...)", () => { + const originalIsNode = Utils.isNode; + + afterEach(() => { + Utils.isNode = originalIsNode; + }); + + runInBothEnvironments("should handle empty string", () => { + const str = Utils.fromUtf8ToB64(""); + expect(str).toBe(""); + }); + + runInBothEnvironments("should convert a normal b64 string", () => { + const str = Utils.fromUtf8ToB64(asciiHelloWorld); + expect(str).toBe(b64HelloWorldString); + }); + + runInBothEnvironments("should convert various special characters", () => { + const cases = [ + { input: "»", output: "wrs=" }, + { input: "¦", output: "wqY=" }, + { input: "£", output: "wqM=" }, + { input: "é", output: "w6k=" }, + { input: "ö", output: "w7Y=" }, + { input: "»»", output: "wrvCuw==" }, + ]; + cases.forEach((c) => { + const utfStr = c.input; + const str = Utils.fromUtf8ToB64(utfStr); + expect(str).toBe(c.output); + }); + }); + }); + + describe("fromB64ToUtf8(...)", () => { + const originalIsNode = Utils.isNode; + + afterEach(() => { + Utils.isNode = originalIsNode; + }); + + runInBothEnvironments("should handle empty string", () => { + const str = Utils.fromB64ToUtf8(""); + expect(str).toBe(""); + }); + + runInBothEnvironments("should convert a normal b64 string", () => { + const str = Utils.fromB64ToUtf8(b64HelloWorldString); + expect(str).toBe(asciiHelloWorld); + }); + + runInBothEnvironments("should handle various special characters", () => { + const cases = [ + { input: "wrs=", output: "»" }, + { input: "wqY=", output: "¦" }, + { input: "wqM=", output: "£" }, + { input: "w6k=", output: "é" }, + { input: "w7Y=", output: "ö" }, + { input: "wrvCuw==", output: "»»" }, + ]; + + cases.forEach((c) => { + const b64Str = c.input; + const str = Utils.fromB64ToUtf8(b64Str); + expect(str).toBe(c.output); + }); + }); + }); }); diff --git a/libs/common/src/platform/misc/utils.ts b/libs/common/src/platform/misc/utils.ts index ef65d2130a0..203a04851c5 100644 --- a/libs/common/src/platform/misc/utils.ts +++ b/libs/common/src/platform/misc/utils.ts @@ -233,7 +233,7 @@ export class Utils { if (Utils.isNode) { return Buffer.from(utfStr, "utf8").toString("base64"); } else { - return decodeURIComponent(escape(Utils.global.btoa(utfStr))); + return BufferLib.from(utfStr, "utf8").toString("base64"); } } @@ -245,7 +245,7 @@ export class Utils { if (Utils.isNode) { return Buffer.from(b64Str, "base64").toString("utf8"); } else { - return decodeURIComponent(escape(Utils.global.atob(b64Str))); + return BufferLib.from(b64Str, "base64").toString("utf8"); } } From c798e1f10d365d584eb75fda40fc93a8115e10ad Mon Sep 17 00:00:00 2001 From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Date: Fri, 18 Apr 2025 21:21:01 +0100 Subject: [PATCH 013/499] [PM-18250]Do not default account credit amount (#13719) * Set the default value to zero * Remove the 20 dollar for org --- .../web/src/app/billing/shared/add-credit-dialog.component.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/web/src/app/billing/shared/add-credit-dialog.component.ts b/apps/web/src/app/billing/shared/add-credit-dialog.component.ts index bfe811f1aa7..45dab542ce8 100644 --- a/apps/web/src/app/billing/shared/add-credit-dialog.component.ts +++ b/apps/web/src/app/billing/shared/add-credit-dialog.component.ts @@ -76,7 +76,7 @@ export class AddCreditDialogComponent implements OnInit { async ngOnInit() { if (this.organizationId != null) { if (this.creditAmount == null) { - this.creditAmount = "20.00"; + this.creditAmount = "0.00"; } this.ppButtonCustomField = "organization_id:" + this.organizationId; const userId = await firstValueFrom( @@ -93,7 +93,7 @@ export class AddCreditDialogComponent implements OnInit { } } else { if (this.creditAmount == null) { - this.creditAmount = "10.00"; + this.creditAmount = "0.00"; } const [userId, email] = await firstValueFrom( this.accountService.activeAccount$.pipe(map((a) => [a?.id, a?.email])), From d4dd8d096b607447a932163119d0fc3fe45f7906 Mon Sep 17 00:00:00 2001 From: Todd Martin <106564991+trmartin4@users.noreply.github.com> Date: Fri, 18 Apr 2025 17:35:42 -0400 Subject: [PATCH 014/499] chore(builds): [PM-20431] Add bit-web to build-web workflow paths * Add bit-web to build-web workflow paths. * Updated to also include bit-common --- .github/workflows/build-cli.yml | 6 ++++-- .github/workflows/build-web.yml | 4 ++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index e113d5c253b..e89ca59a297 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -12,12 +12,13 @@ on: - 'cf-pages' paths: - 'apps/cli/**' + - 'bitwarden_license/bit-cli/**' + - 'bitwarden_license/bit-common/**' - 'libs/**' - '*' - '!*.md' - '!*.txt' - '.github/workflows/build-cli.yml' - - 'bitwarden_license/bit-cli/**' push: branches: - 'main' @@ -25,12 +26,13 @@ on: - 'hotfix-rc-cli' paths: - 'apps/cli/**' + - 'bitwarden_license/bit-cli/**' + - 'bitwarden_license/bit-common/**' - 'libs/**' - '*' - '!*.md' - '!*.txt' - '.github/workflows/build-cli.yml' - - 'bitwarden_license/bit-cli/**' workflow_call: inputs: {} workflow_dispatch: diff --git a/.github/workflows/build-web.yml b/.github/workflows/build-web.yml index 3da524702fe..fb429468134 100644 --- a/.github/workflows/build-web.yml +++ b/.github/workflows/build-web.yml @@ -12,6 +12,8 @@ on: - 'cf-pages' paths: - 'apps/web/**' + - 'bitwarden_license/bit-common/**' + - 'bitwarden_license/bit-web/**' - 'libs/**' - '*' - '!*.md' @@ -24,6 +26,8 @@ on: - 'hotfix-rc-web' paths: - 'apps/web/**' + - 'bitwarden_license/bit-common/**' + - 'bitwarden_license/bit-web/**' - 'libs/**' - '*' - '!*.md' From 5dfa0eb91a014eb3418b0f1ef56a31d55ba06e75 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Sat, 19 Apr 2025 16:10:57 +0200 Subject: [PATCH 015/499] [PM-20277] Disable fingerprint on org invite (#14285) --- .../accept-organization.service.spec.ts | 7 ------- .../accept-organization.service.ts | 11 ----------- 2 files changed, 18 deletions(-) diff --git a/apps/web/src/app/auth/organization-invite/accept-organization.service.spec.ts b/apps/web/src/app/auth/organization-invite/accept-organization.service.spec.ts index cc2d0e371ff..67dbee223d1 100644 --- a/apps/web/src/app/auth/organization-invite/accept-organization.service.spec.ts +++ b/apps/web/src/app/auth/organization-invite/accept-organization.service.spec.ts @@ -24,7 +24,6 @@ import { OrgKey } from "@bitwarden/common/types/key"; import { DialogService } from "@bitwarden/components"; import { KeyService } from "@bitwarden/key-management"; -import { OrganizationTrustComponent } from "../../admin-console/organizations/manage/organization-trust.component"; import { I18nService } from "../../core/i18n.service"; import { @@ -200,11 +199,6 @@ describe("AcceptOrganizationInviteService", () => { encryptedString: "encryptedString", } as EncString); - jest.mock("../../admin-console/organizations/manage/organization-trust.component"); - OrganizationTrustComponent.open = jest.fn().mockReturnValue({ - closed: new BehaviorSubject(true), - }); - await globalState.update(() => invite); policyService.getResetPasswordPolicyOptions.mockReturnValue([ @@ -217,7 +211,6 @@ describe("AcceptOrganizationInviteService", () => { const result = await sut.validateAndAcceptInvite(invite); expect(result).toBe(true); - expect(OrganizationTrustComponent.open).toHaveBeenCalled(); expect(encryptService.encapsulateKeyUnsigned).toHaveBeenCalledWith( { key: "userKey" }, Utils.fromB64ToArray("publicKey"), diff --git a/apps/web/src/app/auth/organization-invite/accept-organization.service.ts b/apps/web/src/app/auth/organization-invite/accept-organization.service.ts index 8b5db9f4872..b6a7719c548 100644 --- a/apps/web/src/app/auth/organization-invite/accept-organization.service.ts +++ b/apps/web/src/app/auth/organization-invite/accept-organization.service.ts @@ -31,8 +31,6 @@ import { OrgKey } from "@bitwarden/common/types/key"; import { DialogService } from "@bitwarden/components"; import { KeyService } from "@bitwarden/key-management"; -import { OrganizationTrustComponent } from "../../admin-console/organizations/manage/organization-trust.component"; - import { OrganizationInvite } from "./organization-invite"; // We're storing the organization invite for 2 reasons: @@ -189,15 +187,6 @@ export class AcceptOrganizationInviteService { } const publicKey = Utils.fromB64ToArray(response.publicKey); - const dialogRef = OrganizationTrustComponent.open(this.dialogService, { - name: invite.organizationName, - orgId: invite.organizationId, - publicKey, - }); - const result = await firstValueFrom(dialogRef.closed); - if (result !== true) { - throw new Error("Organization not trusted, aborting user key rotation"); - } const activeUserId = (await firstValueFrom(this.accountService.activeAccount$)).id; const userKey = await firstValueFrom(this.keyService.userKey$(activeUserId)); From 8de42caf0406aac7ebaa9a3e7654cf7a76604d34 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 19 Apr 2025 18:07:20 +0200 Subject: [PATCH 016/499] [deps] KM: Update Rust crate rsa to v0.9.8 (#12298) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Bernd Schoolmann --- apps/desktop/desktop_native/Cargo.lock | 4 ++-- apps/desktop/desktop_native/Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/desktop/desktop_native/Cargo.lock b/apps/desktop/desktop_native/Cargo.lock index 04bc4887ff7..0884f59e863 100644 --- a/apps/desktop/desktop_native/Cargo.lock +++ b/apps/desktop/desktop_native/Cargo.lock @@ -2455,9 +2455,9 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "rsa" -version = "0.9.6" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d0e5124fcb30e76a7e79bfee683a2746db83784b86289f6251b54b7950a0dfc" +checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b" dependencies = [ "const-oid", "digest", diff --git a/apps/desktop/desktop_native/Cargo.toml b/apps/desktop/desktop_native/Cargo.toml index 01838359147..dc08c2e5a1b 100644 --- a/apps/desktop/desktop_native/Cargo.toml +++ b/apps/desktop/desktop_native/Cargo.toml @@ -38,7 +38,7 @@ oslog = "=0.2.0" pin-project = "=1.1.8" pkcs8 = "=0.10.2" rand = "=0.8.5" -rsa = "=0.9.6" +rsa = "=0.9.8" russh-cryptovec = "=0.7.3" scopeguard = "=1.2.0" security-framework = "=3.1.0" From 86b0a6aa35c3b4bfdca4a3441ad14fd3d929c68a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garc=C3=ADa?= Date: Mon, 21 Apr 2025 12:21:00 +0200 Subject: [PATCH 017/499] Support for logging from NAPI (#14335) * Support for log to electron console from NAPI * Fix test mock --- apps/desktop/desktop_native/Cargo.lock | 1 + apps/desktop/desktop_native/napi/Cargo.toml | 1 + apps/desktop/desktop_native/napi/index.d.ts | 10 ++++ apps/desktop/desktop_native/napi/src/lib.rs | 58 +++++++++++++++++++ .../services/electron-log.main.service.ts | 24 ++++++++ .../services/electron-log.service.spec.ts | 8 +++ 6 files changed, 102 insertions(+) diff --git a/apps/desktop/desktop_native/Cargo.lock b/apps/desktop/desktop_native/Cargo.lock index 0884f59e863..6858011e0cc 100644 --- a/apps/desktop/desktop_native/Cargo.lock +++ b/apps/desktop/desktop_native/Cargo.lock @@ -987,6 +987,7 @@ dependencies = [ "base64", "desktop_core", "hex", + "log", "napi", "napi-build", "napi-derive", diff --git a/apps/desktop/desktop_native/napi/Cargo.toml b/apps/desktop/desktop_native/napi/Cargo.toml index d59581a5e2e..669f166e748 100644 --- a/apps/desktop/desktop_native/napi/Cargo.toml +++ b/apps/desktop/desktop_native/napi/Cargo.toml @@ -18,6 +18,7 @@ base64 = { workspace = true } hex = { workspace = true } anyhow = { workspace = true } desktop_core = { path = "../core" } +log = { workspace = true } napi = { workspace = true, features = ["async"] } napi-derive = { workspace = true } serde = { workspace = true, features = ["derive"] } diff --git a/apps/desktop/desktop_native/napi/index.d.ts b/apps/desktop/desktop_native/napi/index.d.ts index 6ded4e3db93..952f2571c5d 100644 --- a/apps/desktop/desktop_native/napi/index.d.ts +++ b/apps/desktop/desktop_native/napi/index.d.ts @@ -185,3 +185,13 @@ export declare namespace crypto { export declare namespace passkey_authenticator { export function register(): void } +export declare namespace logging { + export const enum LogLevel { + Trace = 0, + Debug = 1, + Info = 2, + Warn = 3, + Error = 4 + } + export function initNapiLog(jsLogFn: (err: Error | null, arg0: LogLevel, arg1: string) => any): void +} diff --git a/apps/desktop/desktop_native/napi/src/lib.rs b/apps/desktop/desktop_native/napi/src/lib.rs index 8cbc526487e..37796ef6f59 100644 --- a/apps/desktop/desktop_native/napi/src/lib.rs +++ b/apps/desktop/desktop_native/napi/src/lib.rs @@ -807,3 +807,61 @@ pub mod passkey_authenticator { }) } } + +#[napi] +pub mod logging { + use log::{Level, Metadata, Record}; + use napi::threadsafe_function::{ + ErrorStrategy::CalleeHandled, ThreadsafeFunction, ThreadsafeFunctionCallMode, + }; + use std::sync::OnceLock; + struct JsLogger(OnceLock>); + static JS_LOGGER: JsLogger = JsLogger(OnceLock::new()); + + #[napi] + pub enum LogLevel { + Trace, + Debug, + Info, + Warn, + Error, + } + + impl From 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, + } + } + } + + #[napi] + pub fn init_napi_log(js_log_fn: ThreadsafeFunction<(LogLevel, String), CalleeHandled>) { + let _ = JS_LOGGER.0.set(js_log_fn); + let _ = log::set_logger(&JS_LOGGER); + log::set_max_level(log::LevelFilter::Debug); + } + + impl log::Log for JsLogger { + fn enabled(&self, metadata: &Metadata) -> bool { + metadata.level() <= log::max_level() + } + + fn log(&self, record: &Record) { + if !self.enabled(record.metadata()) { + return; + } + let Some(logger) = self.0.get() else { + return; + }; + let msg = (record.level().into(), record.args().to_string()); + let _ = logger.call(Ok(msg), ThreadsafeFunctionCallMode::NonBlocking); + } + + fn flush(&self) {} + } +} diff --git a/apps/desktop/src/platform/services/electron-log.main.service.ts b/apps/desktop/src/platform/services/electron-log.main.service.ts index d7100b54825..947f4449271 100644 --- a/apps/desktop/src/platform/services/electron-log.main.service.ts +++ b/apps/desktop/src/platform/services/electron-log.main.service.ts @@ -7,6 +7,7 @@ import log from "electron-log/main"; import { LogLevelType } from "@bitwarden/common/platform/enums/log-level-type.enum"; import { ConsoleLogService as BaseLogService } from "@bitwarden/common/platform/services/console-log.service"; +import { logging } from "@bitwarden/desktop-napi"; import { isDev } from "../../utils"; @@ -30,6 +31,29 @@ export class ElectronLogMainService extends BaseLogService { ipcMain.handle("ipc.log", (_event, { level, message, optionalParams }) => { this.write(level, message, ...optionalParams); }); + + logging.initNapiLog((error, level, message) => this.writeNapiLog(level, message)); + } + + private writeNapiLog(level: logging.LogLevel, message: string) { + let levelType: LogLevelType; + + switch (level) { + case logging.LogLevel.Debug: + levelType = LogLevelType.Debug; + break; + case logging.LogLevel.Warn: + levelType = LogLevelType.Warning; + break; + case logging.LogLevel.Error: + levelType = LogLevelType.Error; + break; + default: + levelType = LogLevelType.Info; + break; + } + + this.write(levelType, "[NAPI] " + message); } write(level: LogLevelType, message?: any, ...optionalParams: any[]) { diff --git a/apps/desktop/src/platform/services/electron-log.service.spec.ts b/apps/desktop/src/platform/services/electron-log.service.spec.ts index 918508977fd..db3093e08e2 100644 --- a/apps/desktop/src/platform/services/electron-log.service.spec.ts +++ b/apps/desktop/src/platform/services/electron-log.service.spec.ts @@ -5,6 +5,14 @@ jest.mock("electron", () => ({ ipcMain: { handle: jest.fn(), on: jest.fn() }, })); +jest.mock("@bitwarden/desktop-napi", () => { + return { + logging: { + initNapiLog: jest.fn(), + }, + }; +}); + describe("ElectronLogMainService", () => { it("sets dev based on electron method", () => { process.env.ELECTRON_IS_DEV = "1"; From 201bdf752b3ff57dba3581d13c026d55db12cdb5 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Mon, 21 Apr 2025 14:14:13 +0200 Subject: [PATCH 018/499] [PM-19728] Device bulk get keys during key rotation (#14216) * Add support for device list endpoint keys during key rotation * Update libs/common/src/auth/abstractions/devices/responses/device.response.ts Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> --------- Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> --- .../auth/abstractions/devices/responses/device.response.ts | 5 +++++ .../services/device-trust.service.implementation.ts | 3 +-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/libs/common/src/auth/abstractions/devices/responses/device.response.ts b/libs/common/src/auth/abstractions/devices/responses/device.response.ts index 84a2fb03c28..6b7f17f65ce 100644 --- a/libs/common/src/auth/abstractions/devices/responses/device.response.ts +++ b/libs/common/src/auth/abstractions/devices/responses/device.response.ts @@ -15,6 +15,9 @@ export class DeviceResponse extends BaseResponse { creationDate: string; revisionDate: string; isTrusted: boolean; + encryptedUserKey: string | null; + encryptedPublicKey: string | null; + devicePendingAuthRequest: DevicePendingAuthRequest | null; constructor(response: any) { @@ -27,6 +30,8 @@ export class DeviceResponse extends BaseResponse { this.creationDate = this.getResponseProperty("CreationDate"); this.revisionDate = this.getResponseProperty("RevisionDate"); this.isTrusted = this.getResponseProperty("IsTrusted"); + this.encryptedUserKey = this.getResponseProperty("EncryptedUserKey"); + this.encryptedPublicKey = this.getResponseProperty("EncryptedPublicKey"); this.devicePendingAuthRequest = this.getResponseProperty("DevicePendingAuthRequest"); } } diff --git a/libs/common/src/key-management/device-trust/services/device-trust.service.implementation.ts b/libs/common/src/key-management/device-trust/services/device-trust.service.implementation.ts index a2211753f4e..c82efa0c571 100644 --- a/libs/common/src/key-management/device-trust/services/device-trust.service.implementation.ts +++ b/libs/common/src/key-management/device-trust/services/device-trust.service.implementation.ts @@ -209,9 +209,8 @@ export class DeviceTrustService implements DeviceTrustServiceAbstraction { devices.data .filter((device) => device.isTrusted) .map(async (device) => { - const deviceWithKeys = await this.devicesApiService.getDeviceKeys(device.identifier); const publicKey = await this.encryptService.decryptToBytes( - deviceWithKeys.encryptedPublicKey, + new EncString(device.encryptedPublicKey), oldUserKey, ); From 83d7ea6aa3e989faef02e3885d4c40693e72a9ac Mon Sep 17 00:00:00 2001 From: Colton Hurst Date: Mon, 21 Apr 2025 09:52:53 -0400 Subject: [PATCH 019/499] [PM-20334] Remove Bindgen from Windows Plugin Authenticator (#14328) * PM-20334: Draft work removing bindgen * PM-20334: Remove comments and address clippy concerns * PM-20334: Edit wpa readme and remove .hpp header file --- apps/desktop/desktop_native/Cargo.lock | 66 ----- .../windows_plugin_authenticator/Cargo.toml | 3 - .../windows_plugin_authenticator/README.md | 20 +- .../windows_plugin_authenticator/build.rs | 27 -- .../pluginauthenticator.hpp | 231 ------------------ .../windows_plugin_authenticator/src/lib.rs | 61 +++-- .../windows_plugin_authenticator/src/pa.rs | 15 -- 7 files changed, 49 insertions(+), 374 deletions(-) delete mode 100644 apps/desktop/desktop_native/windows_plugin_authenticator/build.rs delete mode 100644 apps/desktop/desktop_native/windows_plugin_authenticator/pluginauthenticator.hpp delete mode 100644 apps/desktop/desktop_native/windows_plugin_authenticator/src/pa.rs diff --git a/apps/desktop/desktop_native/Cargo.lock b/apps/desktop/desktop_native/Cargo.lock index 6858011e0cc..c786264a563 100644 --- a/apps/desktop/desktop_native/Cargo.lock +++ b/apps/desktop/desktop_native/Cargo.lock @@ -410,26 +410,6 @@ dependencies = [ "serde", ] -[[package]] -name = "bindgen" -version = "0.71.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3" -dependencies = [ - "bitflags", - "cexpr", - "clang-sys", - "itertools", - "log", - "prettyplease", - "proc-macro2", - "quote", - "regex", - "rustc-hash", - "shlex", - "syn", -] - [[package]] name = "bitflags" version = "2.8.0" @@ -573,15 +553,6 @@ dependencies = [ "shlex", ] -[[package]] -name = "cexpr" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" -dependencies = [ - "nom", -] - [[package]] name = "cfg-if" version = "1.0.0" @@ -622,17 +593,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "clang-sys" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" -dependencies = [ - "glob", - "libc", - "libloading", -] - [[package]] name = "clap" version = "4.5.31" @@ -1493,15 +1453,6 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" -[[package]] -name = "itertools" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" -dependencies = [ - "either", -] - [[package]] name = "itoa" version = "1.0.14" @@ -2303,16 +2254,6 @@ dependencies = [ "zerocopy", ] -[[package]] -name = "prettyplease" -version = "0.2.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6924ced06e1f7dfe3fa48d57b9f74f55d8915f5036121bef647ef4b204895fac" -dependencies = [ - "proc-macro2", - "syn", -] - [[package]] name = "proc-macro-crate" version = "3.2.0" @@ -2491,12 +2432,6 @@ version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" -[[package]] -name = "rustc-hash" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" - [[package]] name = "rustc_version" version = "0.4.1" @@ -3736,7 +3671,6 @@ checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" name = "windows_plugin_authenticator" version = "0.0.0" dependencies = [ - "bindgen", "hex", "windows 0.61.1", "windows-core 0.61.0", diff --git a/apps/desktop/desktop_native/windows_plugin_authenticator/Cargo.toml b/apps/desktop/desktop_native/windows_plugin_authenticator/Cargo.toml index 72a8505389e..18abb86d057 100644 --- a/apps/desktop/desktop_native/windows_plugin_authenticator/Cargo.toml +++ b/apps/desktop/desktop_native/windows_plugin_authenticator/Cargo.toml @@ -5,9 +5,6 @@ edition = { workspace = true } license = { workspace = true } publish = { workspace = true } -[target.'cfg(target_os = "windows")'.build-dependencies] -bindgen = { workspace = true } - [target.'cfg(windows)'.dependencies] windows = { workspace = true, features = ["Win32_Foundation", "Win32_Security", "Win32_System_Com", "Win32_System_LibraryLoader" ] } windows-core = { workspace = true } diff --git a/apps/desktop/desktop_native/windows_plugin_authenticator/README.md b/apps/desktop/desktop_native/windows_plugin_authenticator/README.md index 6dc72ceed46..4802c5d4243 100644 --- a/apps/desktop/desktop_native/windows_plugin_authenticator/README.md +++ b/apps/desktop/desktop_native/windows_plugin_authenticator/README.md @@ -2,22 +2,6 @@ This is an internal crate that's meant to be a safe abstraction layer over the generated Rust bindings for the Windows WebAuthn Plugin Authenticator API's. +This crate is very much a WIP and is not ready for internal use. + You can find more information about the Windows WebAuthn API's [here](https://github.com/microsoft/webauthn). - -## Building - -To build this crate, set the following environment variables: - -- `LIBCLANG_PATH` -> the path to the `bin` directory of your LLVM install ([more info](https://rust-lang.github.io/rust-bindgen/requirements.html?highlight=libclang_path#installing-clang)) - -### Bash Example - -``` -export LIBCLANG_PATH='C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\Llvm\x64\bin' -``` - -### PowerShell Example - -``` -$env:LIBCLANG_PATH = 'C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\Llvm\x64\bin' -``` diff --git a/apps/desktop/desktop_native/windows_plugin_authenticator/build.rs b/apps/desktop/desktop_native/windows_plugin_authenticator/build.rs deleted file mode 100644 index 4843145bac0..00000000000 --- a/apps/desktop/desktop_native/windows_plugin_authenticator/build.rs +++ /dev/null @@ -1,27 +0,0 @@ -fn main() { - #[cfg(target_os = "windows")] - windows(); -} - -#[cfg(target_os = "windows")] -fn windows() { - let out_dir = std::env::var("OUT_DIR").expect("OUT_DIR not set"); - - let bindings = bindgen::Builder::default() - .header("pluginauthenticator.hpp") - .parse_callbacks(Box::new(bindgen::CargoCallbacks::new())) - .allowlist_type("DWORD") - .allowlist_type("PBYTE") - .allowlist_type("EXPERIMENTAL.*") - .allowlist_function(".*EXPERIMENTAL.*") - .allowlist_function("WebAuthNGetApiVersionNumber") - .generate() - .expect("Unable to generate bindings."); - - bindings - .write_to_file(format!( - "{}\\windows_plugin_authenticator_bindings.rs", - out_dir - )) - .expect("Couldn't write bindings."); -} diff --git a/apps/desktop/desktop_native/windows_plugin_authenticator/pluginauthenticator.hpp b/apps/desktop/desktop_native/windows_plugin_authenticator/pluginauthenticator.hpp deleted file mode 100644 index c800266a3e6..00000000000 --- a/apps/desktop/desktop_native/windows_plugin_authenticator/pluginauthenticator.hpp +++ /dev/null @@ -1,231 +0,0 @@ -/* - Bitwarden's pluginauthenticator.hpp - - Source: https://github.com/microsoft/webauthn/blob/master/experimental/pluginauthenticator.h - - This is a C++ header file, so the extension has been manually - changed from `.h` to `.hpp`, so bindgen will automatically - generate the correct C++ bindings. - - More Info: https://rust-lang.github.io/rust-bindgen/cpp.html -*/ - -/* this ALWAYS GENERATED file contains the definitions for the interfaces */ - -/* File created by MIDL compiler version 8.01.0628 */ -/* @@MIDL_FILE_HEADING( ) */ - -/* verify that the version is high enough to compile this file*/ -#ifndef __REQUIRED_RPCNDR_H_VERSION__ -#define __REQUIRED_RPCNDR_H_VERSION__ 501 -#endif - -/* verify that the version is high enough to compile this file*/ -#ifndef __REQUIRED_RPCSAL_H_VERSION__ -#define __REQUIRED_RPCSAL_H_VERSION__ 100 -#endif - -#include "rpc.h" -#include "rpcndr.h" - -#ifndef __RPCNDR_H_VERSION__ -#error this stub requires an updated version of -#endif /* __RPCNDR_H_VERSION__ */ - -#ifndef COM_NO_WINDOWS_H -#include "windows.h" -#include "ole2.h" -#endif /*COM_NO_WINDOWS_H*/ - -#ifndef __pluginauthenticator_h__ -#define __pluginauthenticator_h__ - -#if defined(_MSC_VER) && (_MSC_VER >= 1020) -#pragma once -#endif - -#ifndef DECLSPEC_XFGVIRT -#if defined(_CONTROL_FLOW_GUARD_XFG) -#define DECLSPEC_XFGVIRT(base, func) __declspec(xfg_virtual(base, func)) -#else -#define DECLSPEC_XFGVIRT(base, func) -#endif -#endif - -/* Forward Declarations */ - -#ifndef __EXPERIMENTAL_IPluginAuthenticator_FWD_DEFINED__ -#define __EXPERIMENTAL_IPluginAuthenticator_FWD_DEFINED__ -typedef interface EXPERIMENTAL_IPluginAuthenticator EXPERIMENTAL_IPluginAuthenticator; - -#endif /* __EXPERIMENTAL_IPluginAuthenticator_FWD_DEFINED__ */ - -/* header files for imported files */ -#include "oaidl.h" -#include "webauthn.h" - -#ifdef __cplusplus -extern "C"{ -#endif - -/* interface __MIDL_itf_pluginauthenticator_0000_0000 */ -/* [local] */ - -typedef struct _EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_REQUEST - { - HWND hWnd; - GUID transactionId; - DWORD cbRequestSignature; - /* [size_is] */ byte *pbRequestSignature; - DWORD cbEncodedRequest; - /* [size_is] */ byte *pbEncodedRequest; - } EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_REQUEST; - -typedef struct _EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_REQUEST *EXPERIMENTAL_PWEBAUTHN_PLUGIN_OPERATION_REQUEST; - -typedef const EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_REQUEST *EXPERIMENTAL_PCWEBAUTHN_PLUGIN_OPERATION_REQUEST; - -typedef struct _EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_RESPONSE - { - DWORD cbEncodedResponse; - /* [size_is] */ byte *pbEncodedResponse; - } EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_RESPONSE; - -typedef struct _EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_RESPONSE *EXPERIMENTAL_PWEBAUTHN_PLUGIN_OPERATION_RESPONSE; - -typedef const EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_RESPONSE *EXPERIMENTAL_PCWEBAUTHN_PLUGIN_OPERATION_RESPONSE; - -typedef struct _EXPERIMENTAL_WEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST - { - GUID transactionId; - DWORD cbRequestSignature; - /* [size_is] */ byte *pbRequestSignature; - } EXPERIMENTAL_WEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST; - -typedef struct _EXPERIMENTAL_WEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST *EXPERIMENTAL_PWEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST; - -typedef const EXPERIMENTAL_WEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST *EXPERIMENTAL_PCWEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST; - -extern RPC_IF_HANDLE __MIDL_itf_pluginauthenticator_0000_0000_v0_0_c_ifspec; -extern RPC_IF_HANDLE __MIDL_itf_pluginauthenticator_0000_0000_v0_0_s_ifspec; - -#ifndef __EXPERIMENTAL_IPluginAuthenticator_INTERFACE_DEFINED__ -#define __EXPERIMENTAL_IPluginAuthenticator_INTERFACE_DEFINED__ - -/* interface EXPERIMENTAL_IPluginAuthenticator */ -/* [unique][version][uuid][object] */ - -EXTERN_C const IID IID_EXPERIMENTAL_IPluginAuthenticator; - -#if defined(__cplusplus) && !defined(CINTERFACE) - - MIDL_INTERFACE("e6466e9a-b2f3-47c5-b88d-89bc14a8d998") - EXPERIMENTAL_IPluginAuthenticator : public IUnknown - { - public: - virtual HRESULT STDMETHODCALLTYPE EXPERIMENTAL_PluginMakeCredential( - /* [in] */ __RPC__in EXPERIMENTAL_PCWEBAUTHN_PLUGIN_OPERATION_REQUEST request, - /* [out] */ __RPC__deref_out_opt EXPERIMENTAL_PWEBAUTHN_PLUGIN_OPERATION_RESPONSE *response) = 0; - - virtual HRESULT STDMETHODCALLTYPE EXPERIMENTAL_PluginGetAssertion( - /* [in] */ __RPC__in EXPERIMENTAL_PCWEBAUTHN_PLUGIN_OPERATION_REQUEST request, - /* [out] */ __RPC__deref_out_opt EXPERIMENTAL_PWEBAUTHN_PLUGIN_OPERATION_RESPONSE *response) = 0; - - virtual HRESULT STDMETHODCALLTYPE EXPERIMENTAL_PluginCancelOperation( - /* [in] */ __RPC__in EXPERIMENTAL_PCWEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST request) = 0; - - }; - -#else /* C style interface */ - - typedef struct EXPERIMENTAL_IPluginAuthenticatorVtbl - { - BEGIN_INTERFACE - - DECLSPEC_XFGVIRT(IUnknown, QueryInterface) - HRESULT ( STDMETHODCALLTYPE *QueryInterface )( - __RPC__in EXPERIMENTAL_IPluginAuthenticator * This, - /* [in] */ __RPC__in REFIID riid, - /* [annotation][iid_is][out] */ - _COM_Outptr_ void **ppvObject); - - DECLSPEC_XFGVIRT(IUnknown, AddRef) - ULONG ( STDMETHODCALLTYPE *AddRef )( - __RPC__in EXPERIMENTAL_IPluginAuthenticator * This); - - DECLSPEC_XFGVIRT(IUnknown, Release) - ULONG ( STDMETHODCALLTYPE *Release )( - __RPC__in EXPERIMENTAL_IPluginAuthenticator * This); - - DECLSPEC_XFGVIRT(EXPERIMENTAL_IPluginAuthenticator, EXPERIMENTAL_PluginMakeCredential) - HRESULT ( STDMETHODCALLTYPE *EXPERIMENTAL_PluginMakeCredential )( - __RPC__in EXPERIMENTAL_IPluginAuthenticator * This, - /* [in] */ __RPC__in EXPERIMENTAL_PCWEBAUTHN_PLUGIN_OPERATION_REQUEST request, - /* [out] */ __RPC__deref_out_opt EXPERIMENTAL_PWEBAUTHN_PLUGIN_OPERATION_RESPONSE *response); - - DECLSPEC_XFGVIRT(EXPERIMENTAL_IPluginAuthenticator, EXPERIMENTAL_PluginGetAssertion) - HRESULT ( STDMETHODCALLTYPE *EXPERIMENTAL_PluginGetAssertion )( - __RPC__in EXPERIMENTAL_IPluginAuthenticator * This, - /* [in] */ __RPC__in EXPERIMENTAL_PCWEBAUTHN_PLUGIN_OPERATION_REQUEST request, - /* [out] */ __RPC__deref_out_opt EXPERIMENTAL_PWEBAUTHN_PLUGIN_OPERATION_RESPONSE *response); - - DECLSPEC_XFGVIRT(EXPERIMENTAL_IPluginAuthenticator, EXPERIMENTAL_PluginCancelOperation) - HRESULT ( STDMETHODCALLTYPE *EXPERIMENTAL_PluginCancelOperation )( - __RPC__in EXPERIMENTAL_IPluginAuthenticator * This, - /* [in] */ __RPC__in EXPERIMENTAL_PCWEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST request); - - END_INTERFACE - } EXPERIMENTAL_IPluginAuthenticatorVtbl; - - interface EXPERIMENTAL_IPluginAuthenticator - { - CONST_VTBL struct EXPERIMENTAL_IPluginAuthenticatorVtbl *lpVtbl; - }; - -#ifdef COBJMACROS - - -#define EXPERIMENTAL_IPluginAuthenticator_QueryInterface(This,riid,ppvObject) \ - ( (This)->lpVtbl -> QueryInterface(This,riid,ppvObject) ) - -#define EXPERIMENTAL_IPluginAuthenticator_AddRef(This) \ - ( (This)->lpVtbl -> AddRef(This) ) - -#define EXPERIMENTAL_IPluginAuthenticator_Release(This) \ - ( (This)->lpVtbl -> Release(This) ) - - -#define EXPERIMENTAL_IPluginAuthenticator_EXPERIMENTAL_PluginMakeCredential(This,request,response) \ - ( (This)->lpVtbl -> EXPERIMENTAL_PluginMakeCredential(This,request,response) ) - -#define EXPERIMENTAL_IPluginAuthenticator_EXPERIMENTAL_PluginGetAssertion(This,request,response) \ - ( (This)->lpVtbl -> EXPERIMENTAL_PluginGetAssertion(This,request,response) ) - -#define EXPERIMENTAL_IPluginAuthenticator_EXPERIMENTAL_PluginCancelOperation(This,request) \ - ( (This)->lpVtbl -> EXPERIMENTAL_PluginCancelOperation(This,request) ) - -#endif /* COBJMACROS */ - -#endif /* C style interface */ - -#endif /* __EXPERIMENTAL_IPluginAuthenticator_INTERFACE_DEFINED__ */ - -/* Additional Prototypes for ALL interfaces */ - -unsigned long __RPC_USER HWND_UserSize( __RPC__in unsigned long *, unsigned long , __RPC__in HWND * ); -unsigned char * __RPC_USER HWND_UserMarshal( __RPC__in unsigned long *, __RPC__inout_xcount(0) unsigned char *, __RPC__in HWND * ); -unsigned char * __RPC_USER HWND_UserUnmarshal(__RPC__in unsigned long *, __RPC__in_xcount(0) unsigned char *, __RPC__out HWND * ); -void __RPC_USER HWND_UserFree( __RPC__in unsigned long *, __RPC__in HWND * ); - -unsigned long __RPC_USER HWND_UserSize64( __RPC__in unsigned long *, unsigned long , __RPC__in HWND * ); -unsigned char * __RPC_USER HWND_UserMarshal64( __RPC__in unsigned long *, __RPC__inout_xcount(0) unsigned char *, __RPC__in HWND * ); -unsigned char * __RPC_USER HWND_UserUnmarshal64(__RPC__in unsigned long *, __RPC__in_xcount(0) unsigned char *, __RPC__out HWND * ); -void __RPC_USER HWND_UserFree64( __RPC__in unsigned long *, __RPC__in HWND * ); - -/* end of Additional Prototypes */ - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/apps/desktop/desktop_native/windows_plugin_authenticator/src/lib.rs b/apps/desktop/desktop_native/windows_plugin_authenticator/src/lib.rs index fe2e35df2f8..21257068bd0 100644 --- a/apps/desktop/desktop_native/windows_plugin_authenticator/src/lib.rs +++ b/apps/desktop/desktop_native/windows_plugin_authenticator/src/lib.rs @@ -2,15 +2,6 @@ #![allow(non_snake_case)] #![allow(non_camel_case_types)] -mod pa; - -use pa::{ - DWORD, EXPERIMENTAL_PCWEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST, - EXPERIMENTAL_PCWEBAUTHN_PLUGIN_OPERATION_REQUEST, - EXPERIMENTAL_PWEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_RESPONSE, - EXPERIMENTAL_PWEBAUTHN_PLUGIN_OPERATION_RESPONSE, - EXPERIMENTAL_WEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_RESPONSE, PBYTE, -}; use std::ffi::c_uchar; use std::ptr; use windows::Win32::Foundation::*; @@ -23,11 +14,53 @@ const AUTHENTICATOR_NAME: &str = "Bitwarden Desktop Authenticator"; const CLSID: &str = "0f7dc5d9-69ce-4652-8572-6877fd695062"; const RPID: &str = "bitwarden.com"; -/// Returns the current Windows WebAuthN version. -pub fn get_version_number() -> u32 { - unsafe { pa::WebAuthNGetApiVersionNumber() } +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct EXPERIMENTAL_WEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST { + pub transactionId: GUID, + pub cbRequestSignature: Dword, + pub pbRequestSignature: *mut byte, } +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_REQUEST { + pub hWnd: HWND, + pub transactionId: GUID, + pub cbRequestSignature: Dword, + pub pbRequestSignature: *mut byte, + pub cbEncodedRequest: Dword, + pub pbEncodedRequest: *mut byte, +} + +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct EXPERIMENTAL_WEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_RESPONSE { + pub cbOpSignPubKey: Dword, + pub pbOpSignPubKey: PByte, +} + +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_RESPONSE { + pub cbEncodedResponse: Dword, + pub pbEncodedResponse: *mut byte, +} + +type Dword = u32; +type Byte = u8; +type byte = u8; +pub type PByte = *mut Byte; + +type EXPERIMENTAL_PCWEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST = + *const EXPERIMENTAL_WEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST; +pub type EXPERIMENTAL_PCWEBAUTHN_PLUGIN_OPERATION_REQUEST = + *const EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_REQUEST; +pub type EXPERIMENTAL_PWEBAUTHN_PLUGIN_OPERATION_RESPONSE = + *mut EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_RESPONSE; +pub type EXPERIMENTAL_PWEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_RESPONSE = + *mut EXPERIMENTAL_WEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_RESPONSE; + /// Handles initialization and registration for the Bitwarden desktop app as a /// plugin authenticator with Windows. /// For now, also adds the authenticator @@ -123,9 +156,9 @@ fn add_authenticator() -> std::result::Result<(), String> { pbAuthenticatorInfo: authenticator_info_bytes.as_mut_ptr(), }; - let plugin_signing_public_key_byte_count: DWORD = 0; + let plugin_signing_public_key_byte_count: Dword = 0; let mut plugin_signing_public_key: c_uchar = 0; - let plugin_signing_public_key_ptr: PBYTE = &mut plugin_signing_public_key; + let plugin_signing_public_key_ptr: PByte = &mut plugin_signing_public_key; let mut add_response = EXPERIMENTAL_WEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_RESPONSE { cbOpSignPubKey: plugin_signing_public_key_byte_count, diff --git a/apps/desktop/desktop_native/windows_plugin_authenticator/src/pa.rs b/apps/desktop/desktop_native/windows_plugin_authenticator/src/pa.rs deleted file mode 100644 index 7c93399594d..00000000000 --- a/apps/desktop/desktop_native/windows_plugin_authenticator/src/pa.rs +++ /dev/null @@ -1,15 +0,0 @@ -/* - The 'pa' (plugin authenticator) module will contain the generated - bindgen code. - - The attributes below will suppress warnings from the generated code. -*/ - -#![cfg(target_os = "windows")] -#![allow(clippy::all)] -#![allow(warnings)] - -include!(concat!( - env!("OUT_DIR"), - "/windows_plugin_authenticator_bindings.rs" -)); From 1b6f75806d17c4f2733d15789afa6ec708d49e9a Mon Sep 17 00:00:00 2001 From: Github Actions Date: Mon, 21 Apr 2025 14:22:41 +0000 Subject: [PATCH 020/499] 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/desktop/package.json | 2 +- apps/desktop/src/package-lock.json | 4 ++-- apps/desktop/src/package.json | 2 +- apps/web/package.json | 2 +- package-lock.json | 8 ++++---- 9 files changed, 13 insertions(+), 13 deletions(-) diff --git a/apps/browser/package.json b/apps/browser/package.json index b311b837e78..9ed3c807c11 100644 --- a/apps/browser/package.json +++ b/apps/browser/package.json @@ -1,6 +1,6 @@ { "name": "@bitwarden/browser", - "version": "2025.3.2", + "version": "2025.4.0", "scripts": { "build": "npm run build:chrome", "build:chrome": "cross-env BROWSER=chrome MANIFEST_VERSION=3 NODE_OPTIONS=\"--max-old-space-size=8192\" webpack", diff --git a/apps/browser/src/manifest.json b/apps/browser/src/manifest.json index 35578fb8321..fca62568048 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": "2025.3.2", + "version": "2025.4.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 556a73ac15b..47e0cc465ef 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": "2025.3.2", + "version": "2025.4.0", "description": "__MSG_extDesc__", "default_locale": "en", "author": "Bitwarden Inc.", diff --git a/apps/cli/package.json b/apps/cli/package.json index 04fe4290d31..1bf6a1d41a1 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": "2025.3.0", + "version": "2025.4.0", "keywords": [ "bitwarden", "password", diff --git a/apps/desktop/package.json b/apps/desktop/package.json index ae34deee5b8..9f442da47a1 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": "2025.4.1", + "version": "2025.4.2", "keywords": [ "bitwarden", "password", diff --git a/apps/desktop/src/package-lock.json b/apps/desktop/src/package-lock.json index a720dff7257..f6449bd9626 100644 --- a/apps/desktop/src/package-lock.json +++ b/apps/desktop/src/package-lock.json @@ -1,12 +1,12 @@ { "name": "@bitwarden/desktop", - "version": "2025.4.1", + "version": "2025.4.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@bitwarden/desktop", - "version": "2025.4.1", + "version": "2025.4.2", "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 e288f5f5a79..45a6f6b90af 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": "2025.4.1", + "version": "2025.4.2", "author": "Bitwarden Inc. (https://bitwarden.com)", "homepage": "https://bitwarden.com", "license": "GPL-3.0", diff --git a/apps/web/package.json b/apps/web/package.json index 1f4ae9c29cf..e65848602e9 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@bitwarden/web-vault", - "version": "2025.4.0", + "version": "2025.4.1", "scripts": { "build:oss": "cross-env NODE_OPTIONS=\"--max-old-space-size=8192\" webpack", "build:bit": "cross-env NODE_OPTIONS=\"--max-old-space-size=8192\" webpack -c ../../bitwarden_license/bit-web/webpack.config.js", diff --git a/package-lock.json b/package-lock.json index 65f0c87721a..3e16fd7ba68 100644 --- a/package-lock.json +++ b/package-lock.json @@ -191,11 +191,11 @@ }, "apps/browser": { "name": "@bitwarden/browser", - "version": "2025.3.2" + "version": "2025.4.0" }, "apps/cli": { "name": "@bitwarden/cli", - "version": "2025.3.0", + "version": "2025.4.0", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@koa/multer": "3.0.2", @@ -231,7 +231,7 @@ }, "apps/desktop": { "name": "@bitwarden/desktop", - "version": "2025.4.1", + "version": "2025.4.2", "hasInstallScript": true, "license": "GPL-3.0" }, @@ -245,7 +245,7 @@ }, "apps/web": { "name": "@bitwarden/web-vault", - "version": "2025.4.0" + "version": "2025.4.1" }, "libs/admin-console": { "name": "@bitwarden/admin-console", From a250395e6d0e4efb9404a120a30c5e9730f1bae0 Mon Sep 17 00:00:00 2001 From: Opeyemi Date: Mon, 21 Apr 2025 15:36:36 +0100 Subject: [PATCH 021/499] =?UTF-8?q?BRE-757:=20add=20hold=20label=20for=20R?= =?UTF-8?q?enovate=20PR=20that=20touches=20production=20workf=E2=80=A6=20(?= =?UTF-8?q?#14311)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/renovate.json5 | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/renovate.json5 b/.github/renovate.json5 index c4202ed2a68..ee97f16b0a9 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -49,6 +49,7 @@ "./github/workflows/release-web.yml", ], commitMessagePrefix: "[deps] BRE:", + addLabels: ["hold"], }, { // Disable major and minor updates for TypeScript and Zone.js because they are managed by Angular. From 43b1f5536024693c5de969b9109ae0f123d567d3 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Mon, 21 Apr 2025 16:57:26 +0200 Subject: [PATCH 022/499] [PM-18697] Remove old symmetric key representations in symmetriccryptokey (#13598) * Remove AES128CBC-HMAC encryption * Increase test coverage * Refactor symmetric keys and increase test coverage * Re-add type 0 encryption * Fix ts strict warning * Remove old symmetric key representations in symmetriccryptokey * Fix desktop build * Fix test * Fix build * Update libs/common/src/key-management/crypto/services/web-crypto-function.service.ts Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update libs/node/src/services/node-crypto-function.service.ts Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Undo changes * Remove cast * Undo changes to tests * Fix linting * Undo removing new Uint8Array in aesDecryptFastParameters * Fix merge conflicts * Fix test * Fix another test * Fix test --------- Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> --- .../main-biometrics.service.spec.ts | 2 +- .../os-biometrics-windows.service.ts | 10 +++- .../auth-request/auth-request.service.spec.ts | 6 ++- .../auth-request/auth-request.service.ts | 6 ++- .../encrypt.service.implementation.ts | 24 +++++---- .../crypto/services/encrypt.service.spec.ts | 19 +++++-- .../services/web-crypto-function.service.ts | 50 ++++++++----------- .../services/key-connector.service.spec.ts | 8 ++- .../services/key-connector.service.ts | 8 ++- .../domain/symmetric-crypto-key.spec.ts | 18 ++----- .../models/domain/symmetric-crypto-key.ts | 26 +--------- .../services/node-crypto-function.service.ts | 40 +++++++++------ 12 files changed, 111 insertions(+), 106 deletions(-) diff --git a/apps/desktop/src/key-management/biometrics/main-biometrics.service.spec.ts b/apps/desktop/src/key-management/biometrics/main-biometrics.service.spec.ts index 88dd8c60ed5..09a4dcef4b3 100644 --- a/apps/desktop/src/key-management/biometrics/main-biometrics.service.spec.ts +++ b/apps/desktop/src/key-management/biometrics/main-biometrics.service.spec.ts @@ -300,7 +300,7 @@ describe("MainBiometricsService", function () { expect(userKey).not.toBeNull(); expect(userKey!.keyB64).toBe(biometricKey); - expect(userKey!.encType).toBe(EncryptionType.AesCbc256_HmacSha256_B64); + expect(userKey!.inner().type).toBe(EncryptionType.AesCbc256_HmacSha256_B64); expect(osBiometricsService.getBiometricKey).toHaveBeenCalledWith( "Bitwarden_biometric", `${userId}_user_biometric`, diff --git a/apps/desktop/src/key-management/biometrics/os-biometrics-windows.service.ts b/apps/desktop/src/key-management/biometrics/os-biometrics-windows.service.ts index cd8c94329bc..53647549295 100644 --- a/apps/desktop/src/key-management/biometrics/os-biometrics-windows.service.ts +++ b/apps/desktop/src/key-management/biometrics/os-biometrics-windows.service.ts @@ -1,5 +1,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { EncryptionType } from "@bitwarden/common/platform/enums"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { biometrics, passwords } from "@bitwarden/desktop-napi"; @@ -218,7 +220,13 @@ export default class OsBiometricsServiceWindows implements OsBiometricService { symmetricKey: SymmetricCryptoKey, clientKeyPartB64: string | undefined, ): biometrics.KeyMaterial { - const key = symmetricKey?.macKeyB64 ?? symmetricKey?.keyB64; + let key = null; + const innerKey = symmetricKey.inner(); + if (innerKey.type === EncryptionType.AesCbc256_HmacSha256_B64) { + key = Utils.fromBufferToB64(innerKey.authenticationKey); + } else { + key = Utils.fromBufferToB64(innerKey.encryptionKey); + } const result = { osKeyPartB64: key, 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 b5846fcfdbf..f7bf2260a36 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 @@ -106,7 +106,9 @@ describe("AuthRequestService", () => { }); it("should use the master key and hash if they exist", async () => { - masterPasswordService.masterKeySubject.next({ encKey: new Uint8Array(64) } as MasterKey); + masterPasswordService.masterKeySubject.next( + new SymmetricCryptoKey(new Uint8Array(32)) as MasterKey, + ); masterPasswordService.masterKeyHashSubject.next("MASTER_KEY_HASH"); await sut.approveOrDenyAuthRequest( @@ -115,7 +117,7 @@ describe("AuthRequestService", () => { ); expect(encryptService.encapsulateKeyUnsigned).toHaveBeenCalledWith( - { encKey: new Uint8Array(64) }, + new SymmetricCryptoKey(new Uint8Array(32)), expect.anything(), ); }); 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 f4316c2e519..226403d9c8c 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 @@ -14,6 +14,7 @@ import { AuthRequestPushNotification } from "@bitwarden/common/models/response/n import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; +import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { AUTH_REQUEST_DISK_LOCAL, StateProvider, @@ -120,7 +121,10 @@ export class AuthRequestService implements AuthRequestServiceAbstraction { keyToEncrypt = await this.keyService.getUserKey(); } - const encryptedKey = await this.encryptService.encapsulateKeyUnsigned(keyToEncrypt, pubKey); + const encryptedKey = await this.encryptService.encapsulateKeyUnsigned( + keyToEncrypt as SymmetricCryptoKey, + pubKey, + ); const response = new PasswordlessAuthRequest( encryptedKey.encryptedString, 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 4b299c9c6e6..e9d8c1c30a2 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 @@ -47,7 +47,7 @@ export class EncryptServiceImplementation implements EncryptService { } if (this.blockType0) { - if (key.encType === EncryptionType.AesCbc256_B64 || key.key.byteLength < 64) { + if (key.inner().type === EncryptionType.AesCbc256_B64 || key.key.byteLength < 64) { throw new Error("Type 0 encryption is not supported."); } } @@ -84,7 +84,7 @@ export class EncryptServiceImplementation implements EncryptService { } if (this.blockType0) { - if (key.encType === EncryptionType.AesCbc256_B64 || key.key.byteLength < 64) { + if (key.inner().type === EncryptionType.AesCbc256_B64 || key.key.byteLength < 64) { throw new Error("Type 0 encryption is not supported."); } } @@ -124,7 +124,7 @@ export class EncryptServiceImplementation implements EncryptService { if (encString.encryptionType !== innerKey.type) { this.logDecryptError( "Key encryption type does not match payload encryption type", - key.encType, + innerKey.type, encString.encryptionType, decryptContext, ); @@ -148,7 +148,7 @@ export class EncryptServiceImplementation implements EncryptService { if (!macsEqual) { this.logMacFailed( "decryptToUtf8 MAC comparison failed. Key or payload has changed.", - key.encType, + innerKey.type, encString.encryptionType, decryptContext, ); @@ -191,7 +191,7 @@ export class EncryptServiceImplementation implements EncryptService { if (encThing.encryptionType !== inner.type) { this.logDecryptError( "Encryption key type mismatch", - key.encType, + inner.type, encThing.encryptionType, decryptContext, ); @@ -200,19 +200,23 @@ export class EncryptServiceImplementation implements EncryptService { if (inner.type === EncryptionType.AesCbc256_HmacSha256_B64) { if (encThing.macBytes == null) { - this.logDecryptError("Mac missing", key.encType, encThing.encryptionType, decryptContext); + this.logDecryptError("Mac missing", inner.type, encThing.encryptionType, decryptContext); return null; } const macData = new Uint8Array(encThing.ivBytes.byteLength + encThing.dataBytes.byteLength); macData.set(new Uint8Array(encThing.ivBytes), 0); macData.set(new Uint8Array(encThing.dataBytes), encThing.ivBytes.byteLength); - const computedMac = await this.cryptoFunctionService.hmac(macData, key.macKey, "sha256"); + const computedMac = await this.cryptoFunctionService.hmac( + macData, + inner.authenticationKey, + "sha256", + ); const macsMatch = await this.cryptoFunctionService.compare(encThing.macBytes, computedMac); if (!macsMatch) { this.logMacFailed( "MAC comparison failed. Key or payload has changed.", - key.encType, + inner.type, encThing.encryptionType, decryptContext, ); @@ -222,14 +226,14 @@ export class EncryptServiceImplementation implements EncryptService { return await this.cryptoFunctionService.aesDecrypt( encThing.dataBytes, encThing.ivBytes, - key.encKey, + inner.encryptionKey, "cbc", ); } else if (inner.type === EncryptionType.AesCbc256_B64) { return await this.cryptoFunctionService.aesDecrypt( encThing.dataBytes, encThing.ivBytes, - key.encKey, + inner.encryptionKey, "cbc", ); } diff --git a/libs/common/src/key-management/crypto/services/encrypt.service.spec.ts b/libs/common/src/key-management/crypto/services/encrypt.service.spec.ts index 4cbe3a3da90..fb55fa92605 100644 --- a/libs/common/src/key-management/crypto/services/encrypt.service.spec.ts +++ b/libs/common/src/key-management/crypto/services/encrypt.service.spec.ts @@ -6,7 +6,10 @@ import { EncryptionType } from "@bitwarden/common/platform/enums"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncArrayBuffer } from "@bitwarden/common/platform/models/domain/enc-array-buffer"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; -import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { + Aes256CbcHmacKey, + SymmetricCryptoKey, +} from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { CsprngArray } from "@bitwarden/common/types/csprng"; import { makeStaticByteArray } from "../../../../spec"; @@ -64,6 +67,10 @@ describe("EncryptService", () => { const key = new SymmetricCryptoKey(makeStaticByteArray(32)); const mock32Key = mock(); mock32Key.key = makeStaticByteArray(32); + mock32Key.inner.mockReturnValue({ + type: 0, + encryptionKey: mock32Key.key, + }); await expect(encryptService.encrypt(null!, key)).rejects.toThrow( "Type 0 encryption is not supported.", @@ -146,6 +153,10 @@ describe("EncryptService", () => { const key = new SymmetricCryptoKey(makeStaticByteArray(32)); const mock32Key = mock(); mock32Key.key = makeStaticByteArray(32); + mock32Key.inner.mockReturnValue({ + type: 0, + encryptionKey: mock32Key.key, + }); await expect(encryptService.encryptToBytes(plainValue, key)).rejects.toThrow( "Type 0 encryption is not supported.", @@ -228,7 +239,7 @@ describe("EncryptService", () => { expect(cryptoFunctionService.aesDecrypt).toBeCalledWith( expect.toEqualBuffer(encBuffer.dataBytes), expect.toEqualBuffer(encBuffer.ivBytes), - expect.toEqualBuffer(key.encKey), + expect.toEqualBuffer(key.inner().encryptionKey), "cbc", ); @@ -249,7 +260,7 @@ describe("EncryptService", () => { expect(cryptoFunctionService.aesDecrypt).toBeCalledWith( expect.toEqualBuffer(encBuffer.dataBytes), expect.toEqualBuffer(encBuffer.ivBytes), - expect.toEqualBuffer(key.encKey), + expect.toEqualBuffer(key.inner().encryptionKey), "cbc", ); @@ -267,7 +278,7 @@ describe("EncryptService", () => { expect(cryptoFunctionService.hmac).toBeCalledWith( expect.toEqualBuffer(expectedMacData), - key.macKey, + (key.inner() as Aes256CbcHmacKey).authenticationKey, "sha256", ); diff --git a/libs/common/src/key-management/crypto/services/web-crypto-function.service.ts b/libs/common/src/key-management/crypto/services/web-crypto-function.service.ts index 0c80d508b2d..430774ca2ed 100644 --- a/libs/common/src/key-management/crypto/services/web-crypto-function.service.ts +++ b/libs/common/src/key-management/crypto/services/web-crypto-function.service.ts @@ -1,6 +1,7 @@ import * as argon2 from "argon2-browser"; import * as forge from "node-forge"; +import { EncryptionType } from "../../../platform/enums"; import { Utils } from "../../../platform/misc/utils"; import { CbcDecryptParameters, @@ -247,37 +248,26 @@ export class WebCryptoFunctionService implements CryptoFunctionService { mac: string | null, key: SymmetricCryptoKey, ): CbcDecryptParameters { - const p = {} as CbcDecryptParameters; - if (key.meta != null) { - p.encKey = key.meta.encKeyByteString; - p.macKey = key.meta.macKeyByteString; + const innerKey = key.inner(); + if (innerKey.type === EncryptionType.AesCbc256_B64) { + return { + iv: forge.util.decode64(iv), + data: forge.util.decode64(data), + encKey: forge.util.createBuffer(innerKey.encryptionKey).getBytes(), + } as CbcDecryptParameters; + } else if (innerKey.type === EncryptionType.AesCbc256_HmacSha256_B64) { + const macData = forge.util.decode64(iv) + forge.util.decode64(data); + return { + iv: forge.util.decode64(iv), + data: forge.util.decode64(data), + encKey: forge.util.createBuffer(innerKey.encryptionKey).getBytes(), + macKey: forge.util.createBuffer(innerKey.authenticationKey).getBytes(), + mac: forge.util.decode64(mac!), + macData, + } as CbcDecryptParameters; + } else { + throw new Error("Unsupported encryption type."); } - - if (p.encKey == null) { - p.encKey = forge.util.decode64(key.encKeyB64); - } - p.data = forge.util.decode64(data); - p.iv = forge.util.decode64(iv); - p.macData = p.iv + p.data; - if (p.macKey == null && key.macKeyB64 != null) { - p.macKey = forge.util.decode64(key.macKeyB64); - } - if (mac != null) { - p.mac = forge.util.decode64(mac); - } - - // cache byte string keys for later - if (key.meta == null) { - key.meta = {}; - } - if (key.meta.encKeyByteString == null) { - key.meta.encKeyByteString = p.encKey; - } - if (p.macKey != null && key.meta.macKeyByteString == null) { - key.meta.macKeyByteString = p.macKey; - } - - return p; } aesDecryptFast({ diff --git a/libs/common/src/key-management/key-connector/services/key-connector.service.spec.ts b/libs/common/src/key-management/key-connector/services/key-connector.service.spec.ts index fd3ce0c4777..b88ada56129 100644 --- a/libs/common/src/key-management/key-connector/services/key-connector.service.spec.ts +++ b/libs/common/src/key-management/key-connector/services/key-connector.service.spec.ts @@ -252,7 +252,9 @@ describe("KeyConnectorService", () => { const organization = organizationData(true, true, "https://key-connector-url.com", 2, false); const masterKey = getMockMasterKey(); masterPasswordService.masterKeySubject.next(masterKey); - const keyConnectorRequest = new KeyConnectorUserKeyRequest(masterKey.encKeyB64); + const keyConnectorRequest = new KeyConnectorUserKeyRequest( + Utils.fromBufferToB64(masterKey.inner().encryptionKey), + ); jest.spyOn(keyConnectorService, "getManagingOrganization").mockResolvedValue(organization); jest.spyOn(apiService, "postUserKeyToKeyConnector").mockResolvedValue(); @@ -273,7 +275,9 @@ describe("KeyConnectorService", () => { // Arrange const organization = organizationData(true, true, "https://key-connector-url.com", 2, false); const masterKey = getMockMasterKey(); - const keyConnectorRequest = new KeyConnectorUserKeyRequest(masterKey.encKeyB64); + const keyConnectorRequest = new KeyConnectorUserKeyRequest( + Utils.fromBufferToB64(masterKey.inner().encryptionKey), + ); const error = new Error("Failed to post user key to key connector"); organizationService.organizations$.mockReturnValue(of([organization])); diff --git a/libs/common/src/key-management/key-connector/services/key-connector.service.ts b/libs/common/src/key-management/key-connector/services/key-connector.service.ts index 91b8e9100ac..9799f06f64a 100644 --- a/libs/common/src/key-management/key-connector/services/key-connector.service.ts +++ b/libs/common/src/key-management/key-connector/services/key-connector.service.ts @@ -95,7 +95,9 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction { userId ??= (await firstValueFrom(this.accountService.activeAccount$))?.id; const organization = await this.getManagingOrganization(userId); const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId)); - const keyConnectorRequest = new KeyConnectorUserKeyRequest(masterKey.encKeyB64); + const keyConnectorRequest = new KeyConnectorUserKeyRequest( + Utils.fromBufferToB64(masterKey.inner().encryptionKey), + ); try { await this.apiService.postUserKeyToKeyConnector( @@ -157,7 +159,9 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction { await this.tokenService.getEmail(), kdfConfig, ); - const keyConnectorRequest = new KeyConnectorUserKeyRequest(masterKey.encKeyB64); + const keyConnectorRequest = new KeyConnectorUserKeyRequest( + Utils.fromBufferToB64(masterKey.inner().encryptionKey), + ); await this.masterPasswordService.setMasterKey(masterKey, userId); const userKey = await this.keyService.makeUserKey(masterKey); diff --git a/libs/common/src/platform/models/domain/symmetric-crypto-key.spec.ts b/libs/common/src/platform/models/domain/symmetric-crypto-key.spec.ts index cce99b847bb..6b641ad443a 100644 --- a/libs/common/src/platform/models/domain/symmetric-crypto-key.spec.ts +++ b/libs/common/src/platform/models/domain/symmetric-crypto-key.spec.ts @@ -2,7 +2,7 @@ import { makeStaticByteArray } from "../../../../spec"; import { EncryptionType } from "../../enums"; import { Utils } from "../../misc/utils"; -import { SymmetricCryptoKey } from "./symmetric-crypto-key"; +import { Aes256CbcHmacKey, SymmetricCryptoKey } from "./symmetric-crypto-key"; describe("SymmetricCryptoKey", () => { it("errors if no key", () => { @@ -19,13 +19,8 @@ describe("SymmetricCryptoKey", () => { const cryptoKey = new SymmetricCryptoKey(key); expect(cryptoKey).toEqual({ - encKey: key, - encKeyB64: "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=", - encType: EncryptionType.AesCbc256_B64, key: key, keyB64: "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=", - macKey: null, - macKeyB64: undefined, innerKey: { type: EncryptionType.AesCbc256_B64, encryptionKey: key, @@ -38,14 +33,9 @@ describe("SymmetricCryptoKey", () => { const cryptoKey = new SymmetricCryptoKey(key); expect(cryptoKey).toEqual({ - encKey: key.slice(0, 32), - encKeyB64: "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=", - encType: EncryptionType.AesCbc256_HmacSha256_B64, key: key, keyB64: "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+Pw==", - macKey: key.slice(32, 64), - macKeyB64: "ICEiIyQlJicoKSorLC0uLzAxMjM0NTY3ODk6Ozw9Pj8=", innerKey: { type: EncryptionType.AesCbc256_HmacSha256_B64, encryptionKey: key.slice(0, 32), @@ -86,8 +76,8 @@ describe("SymmetricCryptoKey", () => { expect(actual).toEqual({ type: EncryptionType.AesCbc256_HmacSha256_B64, - encryptionKey: key.encKey, - authenticationKey: key.macKey, + encryptionKey: key.inner().encryptionKey, + authenticationKey: (key.inner() as Aes256CbcHmacKey).authenticationKey, }); }); @@ -95,7 +85,7 @@ describe("SymmetricCryptoKey", () => { const key = new SymmetricCryptoKey(makeStaticByteArray(32)); const actual = key.toEncoded(); - expect(actual).toEqual(key.encKey); + expect(actual).toEqual(key.inner().encryptionKey); }); it("toEncoded returns encoded key for AesCbc256_HmacSha256_B64", () => { diff --git a/libs/common/src/platform/models/domain/symmetric-crypto-key.ts b/libs/common/src/platform/models/domain/symmetric-crypto-key.ts index 45e15c1f602..c85f3432b28 100644 --- a/libs/common/src/platform/models/domain/symmetric-crypto-key.ts +++ b/libs/common/src/platform/models/domain/symmetric-crypto-key.ts @@ -25,15 +25,7 @@ export class SymmetricCryptoKey { private innerKey: Aes256CbcHmacKey | Aes256CbcKey; key: Uint8Array; - encKey: Uint8Array; - macKey?: Uint8Array; - encType: EncryptionType; - keyB64: string; - encKeyB64: string; - macKeyB64: string; - - meta: any; /** * @param key The key in one of the permitted serialization formats @@ -48,30 +40,16 @@ export class SymmetricCryptoKey { type: EncryptionType.AesCbc256_B64, encryptionKey: key, }; - this.encType = EncryptionType.AesCbc256_B64; this.key = key; - this.keyB64 = Utils.fromBufferToB64(this.key); - - this.encKey = key; - this.encKeyB64 = Utils.fromBufferToB64(this.encKey); - - this.macKey = null; - this.macKeyB64 = undefined; + this.keyB64 = this.toBase64(); } else if (key.byteLength === 64) { this.innerKey = { type: EncryptionType.AesCbc256_HmacSha256_B64, encryptionKey: key.slice(0, 32), authenticationKey: key.slice(32), }; - this.encType = EncryptionType.AesCbc256_HmacSha256_B64; this.key = key; - this.keyB64 = Utils.fromBufferToB64(this.key); - - this.encKey = key.slice(0, 32); - this.encKeyB64 = Utils.fromBufferToB64(this.encKey); - - this.macKey = key.slice(32); - this.macKeyB64 = Utils.fromBufferToB64(this.macKey); + this.keyB64 = this.toBase64(); } else { throw new Error(`Unsupported encType/key length ${key.byteLength}`); } diff --git a/libs/node/src/services/node-crypto-function.service.ts b/libs/node/src/services/node-crypto-function.service.ts index 78d72d44104..33ea3adf357 100644 --- a/libs/node/src/services/node-crypto-function.service.ts +++ b/libs/node/src/services/node-crypto-function.service.ts @@ -3,6 +3,7 @@ import * as crypto from "crypto"; import * as forge from "node-forge"; import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service"; +import { EncryptionType } from "@bitwarden/common/platform/enums"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { CbcDecryptParameters, @@ -172,24 +173,33 @@ export class NodeCryptoFunctionService implements CryptoFunctionService { mac: string | null, key: SymmetricCryptoKey, ): CbcDecryptParameters { - const p = {} as CbcDecryptParameters; - p.encKey = key.encKey; - p.data = Utils.fromB64ToArray(data); - p.iv = Utils.fromB64ToArray(iv); + const dataBytes = Utils.fromB64ToArray(data); + const ivBytes = Utils.fromB64ToArray(iv); + const macBytes = mac != null ? Utils.fromB64ToArray(mac) : null; - const macData = new Uint8Array(p.iv.byteLength + p.data.byteLength); - macData.set(new Uint8Array(p.iv), 0); - macData.set(new Uint8Array(p.data), p.iv.byteLength); - p.macData = macData; + const innerKey = key.inner(); - if (key.macKey != null) { - p.macKey = key.macKey; + if (innerKey.type === EncryptionType.AesCbc256_B64) { + return { + iv: ivBytes, + data: dataBytes, + encKey: innerKey.encryptionKey, + } as CbcDecryptParameters; + } else if (innerKey.type === EncryptionType.AesCbc256_HmacSha256_B64) { + const macData = new Uint8Array(ivBytes.byteLength + dataBytes.byteLength); + macData.set(new Uint8Array(ivBytes), 0); + macData.set(new Uint8Array(dataBytes), ivBytes.byteLength); + return { + iv: ivBytes, + data: dataBytes, + mac: macBytes, + macData: macData, + encKey: innerKey.encryptionKey, + macKey: innerKey.authenticationKey, + } as CbcDecryptParameters; + } else { + throw new Error("Unsupported encryption type"); } - if (mac != null) { - p.mac = Utils.fromB64ToArray(mac); - } - - return p; } async aesDecryptFast({ From 143473927ee27d0190740084fccd003b7c4e5268 Mon Sep 17 00:00:00 2001 From: Shane Melton Date: Mon, 21 Apr 2025 08:57:57 -0700 Subject: [PATCH 023/499] [PM-10611] End user notification sync (#14116) * [PM-10611] Remove Angular dependencies from Notifications module * [PM-10611] Move end user notification service to /libs/common/vault/notifications * [PM-10611] Implement listenForEndUserNotifications() for EndUserNotificationService * [PM-10611] Add missing taskId to notification models * [PM-10611] Add switch cases for end user notification payloads * [PM-10611] Mark task related notifications as read when visiting the at-risk password page * [PM-10611] Revert change to default-notifications service * [PM-10611] Fix test * [PM-10611] Fix tests and log warning in case more notifications than the default page size are available * [PM-10611] Use separate feature flag for end user notifications * [PM-10611] Fix test --- .../browser/src/background/main.background.ts | 16 ++ .../at-risk-passwords.component.spec.ts | 14 ++ .../at-risk-passwords.component.ts | 52 +++- .../src/services/jslib-services.module.ts | 19 +- libs/common/src/enums/feature-flag.enum.ts | 2 + .../models/response/notification.response.ts | 6 + .../end-user-notification.service.ts | 13 +- libs/common/src/vault/notifications/index.ts | 2 + .../src/vault}/notifications/models/index.ts | 0 .../models/notification-view.data.ts | 5 +- .../models/notification-view.response.ts | 4 +- .../notifications/models/notification-view.ts | 4 +- ...ault-end-user-notification.service.spec.ts | 223 ++++++++++++++++++ .../default-end-user-notification.service.ts | 213 +++++++++++++++++ .../state/end-user-notification.state.ts | 0 libs/vault/src/index.ts | 1 - libs/vault/src/notifications/index.ts | 2 - ...ault-end-user-notification.service.spec.ts | 200 ---------------- .../default-end-user-notification.service.ts | 125 ---------- 19 files changed, 557 insertions(+), 344 deletions(-) rename libs/{vault/src => common/src/vault}/notifications/abstractions/end-user-notification.service.ts (64%) create mode 100644 libs/common/src/vault/notifications/index.ts rename libs/{vault/src => common/src/vault}/notifications/models/index.ts (100%) rename libs/{vault/src => common/src/vault}/notifications/models/notification-view.data.ts (85%) rename libs/{vault/src => common/src/vault}/notifications/models/notification-view.response.ts (81%) rename libs/{vault/src => common/src/vault}/notifications/models/notification-view.ts (75%) create mode 100644 libs/common/src/vault/notifications/services/default-end-user-notification.service.spec.ts create mode 100644 libs/common/src/vault/notifications/services/default-end-user-notification.service.ts rename libs/{vault/src => common/src/vault}/notifications/state/end-user-notification.state.ts (100%) delete mode 100644 libs/vault/src/notifications/index.ts delete mode 100644 libs/vault/src/notifications/services/default-end-user-notification.service.spec.ts delete mode 100644 libs/vault/src/notifications/services/default-end-user-notification.service.ts diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 2b6827aafa4..3066ef5eef5 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -191,6 +191,10 @@ import { InternalFolderService as InternalFolderServiceAbstraction } from "@bitw import { TotpService as TotpServiceAbstraction } from "@bitwarden/common/vault/abstractions/totp.service"; import { VaultSettingsService as VaultSettingsServiceAbstraction } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { + DefaultEndUserNotificationService, + EndUserNotificationService, +} from "@bitwarden/common/vault/notifications"; import { CipherAuthorizationService, DefaultCipherAuthorizationService, @@ -402,6 +406,7 @@ export default class MainBackground { sdkService: SdkService; sdkLoadService: SdkLoadService; cipherAuthorizationService: CipherAuthorizationService; + endUserNotificationService: EndUserNotificationService; inlineMenuFieldQualificationService: InlineMenuFieldQualificationService; taskService: TaskService; @@ -1320,6 +1325,14 @@ export default class MainBackground { this.ipcContentScriptManagerService = new IpcContentScriptManagerService(this.configService); this.ipcService = new IpcBackgroundService(this.logService); + + this.endUserNotificationService = new DefaultEndUserNotificationService( + this.stateProvider, + this.apiService, + this.notificationsService, + this.authService, + this.logService, + ); } async bootstrap() { @@ -1406,6 +1419,9 @@ export default class MainBackground { this.taskService.listenForTaskNotifications(); } + if (await this.configService.getFeatureFlag(FeatureFlag.EndUserNotifications)) { + this.endUserNotificationService.listenForEndUserNotifications(); + } resolve(); }, 500); }); diff --git a/apps/browser/src/vault/popup/components/at-risk-passwords/at-risk-passwords.component.spec.ts b/apps/browser/src/vault/popup/components/at-risk-passwords/at-risk-passwords.component.spec.ts index 25bf3ce3716..ff583061684 100644 --- a/apps/browser/src/vault/popup/components/at-risk-passwords/at-risk-passwords.component.spec.ts +++ b/apps/browser/src/vault/popup/components/at-risk-passwords/at-risk-passwords.component.spec.ts @@ -12,10 +12,13 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv import { AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants"; import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; import { InlineMenuVisibilitySetting } from "@bitwarden/common/autofill/types"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { EndUserNotificationService } from "@bitwarden/common/vault/notifications"; +import { NotificationView } from "@bitwarden/common/vault/notifications/models"; import { SecurityTask, SecurityTaskType, TaskService } from "@bitwarden/common/vault/tasks"; import { DialogService, ToastService } from "@bitwarden/components"; import { @@ -66,6 +69,7 @@ describe("AtRiskPasswordsComponent", () => { let mockTasks$: BehaviorSubject; let mockCiphers$: BehaviorSubject; let mockOrgs$: BehaviorSubject; + let mockNotifications$: BehaviorSubject; let mockInlineMenuVisibility$: BehaviorSubject; let calloutDismissed$: BehaviorSubject; const setInlineMenuVisibility = jest.fn(); @@ -73,6 +77,7 @@ describe("AtRiskPasswordsComponent", () => { const mockAtRiskPasswordPageService = mock(); const mockChangeLoginPasswordService = mock(); const mockDialogService = mock(); + const mockConfigService = mock(); beforeEach(async () => { mockTasks$ = new BehaviorSubject([ @@ -101,6 +106,7 @@ describe("AtRiskPasswordsComponent", () => { name: "Org 1", } as Organization, ]); + mockNotifications$ = new BehaviorSubject([]); mockInlineMenuVisibility$ = new BehaviorSubject( AutofillOverlayVisibility.Off, @@ -110,6 +116,7 @@ describe("AtRiskPasswordsComponent", () => { setInlineMenuVisibility.mockClear(); mockToastService.showToast.mockClear(); mockDialogService.open.mockClear(); + mockConfigService.getFeatureFlag.mockClear(); mockAtRiskPasswordPageService.isCalloutDismissed.mockReturnValue(calloutDismissed$); await TestBed.configureTestingModule({ @@ -133,6 +140,12 @@ describe("AtRiskPasswordsComponent", () => { cipherViews$: () => mockCiphers$, }, }, + { + provide: EndUserNotificationService, + useValue: { + unreadNotifications$: () => mockNotifications$, + }, + }, { provide: I18nService, useValue: { t: (key: string) => key } }, { provide: AccountService, useValue: { activeAccount$: of({ id: "user" }) } }, { provide: PlatformUtilsService, useValue: mock() }, @@ -145,6 +158,7 @@ describe("AtRiskPasswordsComponent", () => { }, }, { provide: ToastService, useValue: mockToastService }, + { provide: ConfigService, useValue: mockConfigService }, ], }) .overrideModule(JslibModule, { diff --git a/apps/browser/src/vault/popup/components/at-risk-passwords/at-risk-passwords.component.ts b/apps/browser/src/vault/popup/components/at-risk-passwords/at-risk-passwords.component.ts index 37c445f6c30..1b43151193a 100644 --- a/apps/browser/src/vault/popup/components/at-risk-passwords/at-risk-passwords.component.ts +++ b/apps/browser/src/vault/popup/components/at-risk-passwords/at-risk-passwords.component.ts @@ -1,7 +1,19 @@ import { CommonModule } from "@angular/common"; -import { Component, inject, OnInit, signal } from "@angular/core"; +import { Component, DestroyRef, inject, OnInit, signal } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { Router } from "@angular/router"; -import { combineLatest, firstValueFrom, map, of, shareReplay, startWith, switchMap } from "rxjs"; +import { + combineLatest, + concat, + concatMap, + firstValueFrom, + map, + of, + shareReplay, + startWith, + switchMap, + take, +} from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { @@ -11,10 +23,13 @@ import { import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants"; import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { EndUserNotificationService } from "@bitwarden/common/vault/notifications"; import { SecurityTaskType, TaskService } from "@bitwarden/common/vault/tasks"; import { filterOutNullish } from "@bitwarden/common/vault/utils/observable-utilities"; import { @@ -81,6 +96,9 @@ export class AtRiskPasswordsComponent implements OnInit { private changeLoginPasswordService = inject(ChangeLoginPasswordService); private platformUtilsService = inject(PlatformUtilsService); private dialogService = inject(DialogService); + private endUserNotificationService = inject(EndUserNotificationService); + private configService = inject(ConfigService); + private destroyRef = inject(DestroyRef); /** * The cipher that is currently being launched. Used to show a loading spinner on the badge button. @@ -180,6 +198,36 @@ export class AtRiskPasswordsComponent implements OnInit { await this.atRiskPasswordPageService.dismissGettingStarted(userId); } } + + if (await this.configService.getFeatureFlag(FeatureFlag.EndUserNotifications)) { + this.markTaskNotificationsAsRead(); + } + } + + private markTaskNotificationsAsRead() { + this.activeUserData$ + .pipe( + switchMap(({ tasks, userId }) => { + return this.endUserNotificationService.unreadNotifications$(userId).pipe( + take(1), + map((notifications) => { + return notifications.filter((notification) => { + return tasks.some((task) => task.id === notification.taskId); + }); + }), + concatMap((unreadTaskNotifications) => { + // TODO: Investigate creating a bulk endpoint to mark notifications as read + return concat( + ...unreadTaskNotifications.map((n) => + this.endUserNotificationService.markAsRead(n.id, userId), + ), + ); + }), + ); + }), + takeUntilDestroyed(this.destroyRef), + ) + .subscribe(); } async viewCipher(cipher: CipherView) { diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 8e2b3409593..1cc2b591412 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -270,6 +270,10 @@ import { } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { TotpService as TotpServiceAbstraction } from "@bitwarden/common/vault/abstractions/totp.service"; import { VaultSettingsService as VaultSettingsServiceAbstraction } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service"; +import { + DefaultEndUserNotificationService, + EndUserNotificationService, +} from "@bitwarden/common/vault/notifications"; import { CipherAuthorizationService, DefaultCipherAuthorizationService, @@ -306,12 +310,7 @@ import { UserAsymmetricKeysRegenerationService, } from "@bitwarden/key-management"; import { SafeInjectionToken } from "@bitwarden/ui-common"; -import { - DefaultEndUserNotificationService, - EndUserNotificationService, - NewDeviceVerificationNoticeService, - PasswordRepromptService, -} from "@bitwarden/vault"; +import { NewDeviceVerificationNoticeService, PasswordRepromptService } from "@bitwarden/vault"; import { IndividualVaultExportService, IndividualVaultExportServiceAbstraction, @@ -1489,7 +1488,13 @@ const safeProviders: SafeProvider[] = [ safeProvider({ provide: EndUserNotificationService, useClass: DefaultEndUserNotificationService, - deps: [StateProvider, ApiServiceAbstraction, NotificationsService], + deps: [ + StateProvider, + ApiServiceAbstraction, + NotificationsService, + AuthServiceAbstraction, + LogService, + ], }), safeProvider({ provide: DeviceTrustToastServiceAbstraction, diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index 1d0b1521db6..08906c792fb 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -55,6 +55,7 @@ export enum FeatureFlag { VaultBulkManagementAction = "vault-bulk-management-action", SecurityTasks = "security-tasks", CipherKeyEncryption = "cipher-key-encryption", + EndUserNotifications = "pm-10609-end-user-notifications", /* Platform */ IpcChannelFramework = "ipc-channel-framework", @@ -105,6 +106,7 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.VaultBulkManagementAction]: FALSE, [FeatureFlag.SecurityTasks]: FALSE, [FeatureFlag.CipherKeyEncryption]: FALSE, + [FeatureFlag.EndUserNotifications]: FALSE, /* Auth */ [FeatureFlag.PM9112_DeviceApprovalPersistence]: FALSE, diff --git a/libs/common/src/models/response/notification.response.ts b/libs/common/src/models/response/notification.response.ts index aa0ecc97b58..d1bf96b1956 100644 --- a/libs/common/src/models/response/notification.response.ts +++ b/libs/common/src/models/response/notification.response.ts @@ -1,3 +1,5 @@ +import { NotificationViewResponse as EndUserNotificationResponse } from "@bitwarden/common/vault/notifications/models"; + import { NotificationType } from "../../enums"; import { BaseResponse } from "./base.response"; @@ -57,6 +59,10 @@ export class NotificationResponse extends BaseResponse { case NotificationType.SyncOrganizationCollectionSettingChanged: this.payload = new OrganizationCollectionSettingChangedPushNotification(payload); break; + case NotificationType.Notification: + case NotificationType.NotificationStatus: + this.payload = new EndUserNotificationResponse(payload); + break; default: break; } diff --git a/libs/vault/src/notifications/abstractions/end-user-notification.service.ts b/libs/common/src/vault/notifications/abstractions/end-user-notification.service.ts similarity index 64% rename from libs/vault/src/notifications/abstractions/end-user-notification.service.ts rename to libs/common/src/vault/notifications/abstractions/end-user-notification.service.ts index fe2852994f7..bc5dd4d97a4 100644 --- a/libs/vault/src/notifications/abstractions/end-user-notification.service.ts +++ b/libs/common/src/vault/notifications/abstractions/end-user-notification.service.ts @@ -1,6 +1,6 @@ -import { Observable } from "rxjs"; +import { Observable, Subscription } from "rxjs"; -import { UserId } from "@bitwarden/common/types/guid"; +import { NotificationId, UserId } from "@bitwarden/common/types/guid"; import { NotificationView } from "../models"; @@ -25,18 +25,23 @@ export abstract class EndUserNotificationService { * @param notificationId * @param userId */ - abstract markAsRead(notificationId: any, userId: UserId): Promise; + abstract markAsRead(notificationId: NotificationId, userId: UserId): Promise; /** * Mark a notification as deleted. * @param notificationId * @param userId */ - abstract markAsDeleted(notificationId: any, userId: UserId): Promise; + abstract markAsDeleted(notificationId: NotificationId, userId: UserId): Promise; /** * Clear all notifications from state for the given user. * @param userId */ abstract clearState(userId: UserId): Promise; + + /** + * Creates a subscription to listen for end user push notifications and notification status updates. + */ + abstract listenForEndUserNotifications(): Subscription; } diff --git a/libs/common/src/vault/notifications/index.ts b/libs/common/src/vault/notifications/index.ts new file mode 100644 index 00000000000..768262be943 --- /dev/null +++ b/libs/common/src/vault/notifications/index.ts @@ -0,0 +1,2 @@ +export { EndUserNotificationService } from "./abstractions/end-user-notification.service"; +export { DefaultEndUserNotificationService } from "./services/default-end-user-notification.service"; diff --git a/libs/vault/src/notifications/models/index.ts b/libs/common/src/vault/notifications/models/index.ts similarity index 100% rename from libs/vault/src/notifications/models/index.ts rename to libs/common/src/vault/notifications/models/index.ts diff --git a/libs/vault/src/notifications/models/notification-view.data.ts b/libs/common/src/vault/notifications/models/notification-view.data.ts similarity index 85% rename from libs/vault/src/notifications/models/notification-view.data.ts rename to libs/common/src/vault/notifications/models/notification-view.data.ts index 07c147052ad..60314a44684 100644 --- a/libs/vault/src/notifications/models/notification-view.data.ts +++ b/libs/common/src/vault/notifications/models/notification-view.data.ts @@ -1,6 +1,6 @@ import { Jsonify } from "type-fest"; -import { NotificationId } from "@bitwarden/common/types/guid"; +import { NotificationId, SecurityTaskId } from "@bitwarden/common/types/guid"; import { NotificationViewResponse } from "./notification-view.response"; @@ -10,6 +10,7 @@ export class NotificationViewData { title: string; body: string; date: Date; + taskId?: SecurityTaskId; readDate: Date | null; deletedDate: Date | null; @@ -19,6 +20,7 @@ export class NotificationViewData { this.title = response.title; this.body = response.body; this.date = response.date; + this.taskId = response.taskId; this.readDate = response.readDate; this.deletedDate = response.deletedDate; } @@ -30,6 +32,7 @@ export class NotificationViewData { title: obj.title, body: obj.body, date: new Date(obj.date), + taskId: obj.taskId, readDate: obj.readDate ? new Date(obj.readDate) : null, deletedDate: obj.deletedDate ? new Date(obj.deletedDate) : null, }); diff --git a/libs/vault/src/notifications/models/notification-view.response.ts b/libs/common/src/vault/notifications/models/notification-view.response.ts similarity index 81% rename from libs/vault/src/notifications/models/notification-view.response.ts rename to libs/common/src/vault/notifications/models/notification-view.response.ts index bbebf25bd4e..b4b7d8d94cc 100644 --- a/libs/vault/src/notifications/models/notification-view.response.ts +++ b/libs/common/src/vault/notifications/models/notification-view.response.ts @@ -1,5 +1,5 @@ import { BaseResponse } from "@bitwarden/common/models/response/base.response"; -import { NotificationId } from "@bitwarden/common/types/guid"; +import { NotificationId, SecurityTaskId } from "@bitwarden/common/types/guid"; export class NotificationViewResponse extends BaseResponse { id: NotificationId; @@ -7,6 +7,7 @@ export class NotificationViewResponse extends BaseResponse { title: string; body: string; date: Date; + taskId?: SecurityTaskId; readDate: Date; deletedDate: Date; @@ -17,6 +18,7 @@ export class NotificationViewResponse extends BaseResponse { this.title = this.getResponseProperty("Title"); this.body = this.getResponseProperty("Body"); this.date = this.getResponseProperty("Date"); + this.taskId = this.getResponseProperty("TaskId"); this.readDate = this.getResponseProperty("ReadDate"); this.deletedDate = this.getResponseProperty("DeletedDate"); } diff --git a/libs/vault/src/notifications/models/notification-view.ts b/libs/common/src/vault/notifications/models/notification-view.ts similarity index 75% rename from libs/vault/src/notifications/models/notification-view.ts rename to libs/common/src/vault/notifications/models/notification-view.ts index b577a889d05..21d55ec0aed 100644 --- a/libs/vault/src/notifications/models/notification-view.ts +++ b/libs/common/src/vault/notifications/models/notification-view.ts @@ -1,4 +1,4 @@ -import { NotificationId } from "@bitwarden/common/types/guid"; +import { NotificationId, SecurityTaskId } from "@bitwarden/common/types/guid"; export class NotificationView { id: NotificationId; @@ -6,6 +6,7 @@ export class NotificationView { title: string; body: string; date: Date; + taskId?: SecurityTaskId; readDate: Date | null; deletedDate: Date | null; @@ -15,6 +16,7 @@ export class NotificationView { this.title = obj.title; this.body = obj.body; this.date = obj.date; + this.taskId = obj.taskId; this.readDate = obj.readDate; this.deletedDate = obj.deletedDate; } diff --git a/libs/common/src/vault/notifications/services/default-end-user-notification.service.spec.ts b/libs/common/src/vault/notifications/services/default-end-user-notification.service.spec.ts new file mode 100644 index 00000000000..89a78d6f7d2 --- /dev/null +++ b/libs/common/src/vault/notifications/services/default-end-user-notification.service.spec.ts @@ -0,0 +1,223 @@ +import { mock } from "jest-mock-extended"; +import { firstValueFrom, of } from "rxjs"; + +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { NotificationsService } from "@bitwarden/common/platform/notifications"; +import { StateProvider } from "@bitwarden/common/platform/state"; +import { NotificationId, UserId } from "@bitwarden/common/types/guid"; + +import { FakeStateProvider, mockAccountServiceWith } from "../../../../spec"; +import { NotificationViewResponse } from "../models"; +import { NOTIFICATIONS } from "../state/end-user-notification.state"; + +import { + DEFAULT_NOTIFICATION_PAGE_SIZE, + DefaultEndUserNotificationService, +} from "./default-end-user-notification.service"; + +describe("End User Notification Center Service", () => { + let fakeStateProvider: FakeStateProvider; + let mockApiService: jest.Mocked; + let mockNotificationsService: jest.Mocked; + let mockAuthService: jest.Mocked; + let mockLogService: jest.Mocked; + let service: DefaultEndUserNotificationService; + + beforeEach(() => { + fakeStateProvider = new FakeStateProvider(mockAccountServiceWith("user-id" as UserId)); + mockApiService = { + send: jest.fn(), + } as any; + mockNotificationsService = { + notifications$: of(null), + } as any; + mockAuthService = { + authStatuses$: of({}), + } as any; + mockLogService = mock(); + + service = new DefaultEndUserNotificationService( + fakeStateProvider as unknown as StateProvider, + mockApiService, + mockNotificationsService, + mockAuthService, + mockLogService, + ); + }); + + describe("notifications$", () => { + it("should return notifications from state when not null", async () => { + fakeStateProvider.singleUser.mockFor("user-id" as UserId, NOTIFICATIONS, [ + { + id: "notification-id" as NotificationId, + } as NotificationViewResponse, + ]); + + const result = await firstValueFrom(service.notifications$("user-id" as UserId)); + + expect(result.length).toBe(1); + expect(mockApiService.send).not.toHaveBeenCalled(); + expect(mockLogService.warning).not.toHaveBeenCalled(); + }); + + it("should return notifications API when state is null", async () => { + mockApiService.send.mockResolvedValue({ + data: [ + { + id: "notification-id", + }, + ] as NotificationViewResponse[], + }); + + fakeStateProvider.singleUser.mockFor("user-id" as UserId, NOTIFICATIONS, null as any); + + const result = await firstValueFrom(service.notifications$("user-id" as UserId)); + + expect(result.length).toBe(1); + expect(mockApiService.send).toHaveBeenCalledWith( + "GET", + `/notifications?pageSize=${DEFAULT_NOTIFICATION_PAGE_SIZE}`, + null, + true, + true, + ); + expect(mockLogService.warning).not.toHaveBeenCalled(); + }); + + it("should log a warning if there are more notifications available", async () => { + mockApiService.send.mockResolvedValue({ + data: [ + ...new Array(DEFAULT_NOTIFICATION_PAGE_SIZE + 1).fill({ id: "notification-id" }), + ] as NotificationViewResponse[], + continuationToken: "next-token", // Presence of continuation token indicates more data + }); + + fakeStateProvider.singleUser.mockFor("user-id" as UserId, NOTIFICATIONS, null as any); + + const result = await firstValueFrom(service.notifications$("user-id" as UserId)); + + expect(result.length).toBe(DEFAULT_NOTIFICATION_PAGE_SIZE + 1); + expect(mockApiService.send).toHaveBeenCalledWith( + "GET", + `/notifications?pageSize=${DEFAULT_NOTIFICATION_PAGE_SIZE}`, + null, + true, + true, + ); + expect(mockLogService.warning).toHaveBeenCalledWith( + `More notifications available, but not fetched. Consider increasing the page size from ${DEFAULT_NOTIFICATION_PAGE_SIZE}`, + ); + }); + + it("should share the same observable for the same user", async () => { + const first = service.notifications$("user-id" as UserId); + const second = service.notifications$("user-id" as UserId); + + expect(first).toBe(second); + }); + }); + + describe("unreadNotifications$", () => { + it("should return unread notifications from state when read value is null", async () => { + fakeStateProvider.singleUser.mockFor("user-id" as UserId, NOTIFICATIONS, [ + { + id: "notification-id" as NotificationId, + readDate: null as any, + } as NotificationViewResponse, + ]); + + const result = await firstValueFrom(service.unreadNotifications$("user-id" as UserId)); + + expect(result.length).toBe(1); + expect(mockApiService.send).not.toHaveBeenCalled(); + }); + }); + + describe("getNotifications", () => { + it("should call getNotifications returning notifications from API", async () => { + mockApiService.send.mockResolvedValue({ + data: [ + { + id: "notification-id", + }, + ] as NotificationViewResponse[], + }); + + await service.refreshNotifications("user-id" as UserId); + + expect(mockApiService.send).toHaveBeenCalledWith( + "GET", + `/notifications?pageSize=${DEFAULT_NOTIFICATION_PAGE_SIZE}`, + null, + true, + true, + ); + }); + + it("should update local state when notifications are updated", async () => { + mockApiService.send.mockResolvedValue({ + data: [ + { + id: "notification-id", + }, + ] as NotificationViewResponse[], + }); + + const mock = fakeStateProvider.singleUser.mockFor( + "user-id" as UserId, + NOTIFICATIONS, + null as any, + ); + + await service.refreshNotifications("user-id" as UserId); + + expect(mock.nextMock).toHaveBeenCalledWith([ + expect.objectContaining({ + id: "notification-id" as NotificationId, + } as NotificationViewResponse), + ]); + }); + }); + + describe("clear", () => { + it("should clear the local notification state for the user", async () => { + const mock = fakeStateProvider.singleUser.mockFor("user-id" as UserId, NOTIFICATIONS, [ + { + id: "notification-id" as NotificationId, + } as NotificationViewResponse, + ]); + + await service.clearState("user-id" as UserId); + + expect(mock.nextMock).toHaveBeenCalledWith([]); + }); + }); + + describe("markAsDeleted", () => { + it("should send an API request to mark the notification as deleted", async () => { + await service.markAsDeleted("notification-id" as NotificationId, "user-id" as UserId); + expect(mockApiService.send).toHaveBeenCalledWith( + "DELETE", + "/notifications/notification-id/delete", + null, + true, + false, + ); + }); + }); + + describe("markAsRead", () => { + it("should send an API request to mark the notification as read", async () => { + await service.markAsRead("notification-id" as NotificationId, "user-id" as UserId); + expect(mockApiService.send).toHaveBeenCalledWith( + "PATCH", + "/notifications/notification-id/read", + null, + true, + false, + ); + }); + }); +}); diff --git a/libs/common/src/vault/notifications/services/default-end-user-notification.service.ts b/libs/common/src/vault/notifications/services/default-end-user-notification.service.ts new file mode 100644 index 00000000000..87f97b48c27 --- /dev/null +++ b/libs/common/src/vault/notifications/services/default-end-user-notification.service.ts @@ -0,0 +1,213 @@ +import { concatMap, EMPTY, filter, map, Observable, Subscription, switchMap } from "rxjs"; + +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; +import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; +import { NotificationType } from "@bitwarden/common/enums"; +import { ListResponse } from "@bitwarden/common/models/response/list.response"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { NotificationsService } from "@bitwarden/common/platform/notifications"; +import { StateProvider } from "@bitwarden/common/platform/state"; +import { NotificationId, UserId } from "@bitwarden/common/types/guid"; +import { + filterOutNullish, + perUserCache$, +} from "@bitwarden/common/vault/utils/observable-utilities"; + +import { EndUserNotificationService } from "../abstractions/end-user-notification.service"; +import { NotificationView, NotificationViewData, NotificationViewResponse } from "../models"; +import { NOTIFICATIONS } from "../state/end-user-notification.state"; + +/** + * The default number of notifications to fetch from the API. + */ +export const DEFAULT_NOTIFICATION_PAGE_SIZE = 50; + +const getLoggedInUserIds = map, UserId[]>((authStatuses) => + Object.entries(authStatuses ?? {}) + .filter(([, status]) => status >= AuthenticationStatus.Locked) + .map(([userId]) => userId as UserId), +); + +/** + * A service for retrieving and managing notifications for end users. + */ +export class DefaultEndUserNotificationService implements EndUserNotificationService { + constructor( + private stateProvider: StateProvider, + private apiService: ApiService, + private notificationService: NotificationsService, + private authService: AuthService, + private logService: LogService, + ) {} + + notifications$ = perUserCache$((userId: UserId): Observable => { + return this.notificationState(userId).state$.pipe( + switchMap(async (notifications) => { + if (notifications == null) { + await this.fetchNotificationsFromApi(userId); + return null; + } + return notifications; + }), + filterOutNullish(), + map((notifications) => + notifications.map((notification) => new NotificationView(notification)), + ), + ); + }); + + unreadNotifications$ = perUserCache$((userId: UserId): Observable => { + return this.notifications$(userId).pipe( + map((notifications) => notifications.filter((notification) => notification.readDate == null)), + ); + }); + + async markAsRead(notificationId: NotificationId, userId: UserId): Promise { + await this.apiService.send("PATCH", `/notifications/${notificationId}/read`, null, true, false); + await this.notificationState(userId).update((current) => { + const notification = current?.find((n) => n.id === notificationId); + if (notification) { + notification.readDate = new Date(); + } + return current; + }); + } + + async markAsDeleted(notificationId: NotificationId, userId: UserId): Promise { + await this.apiService.send( + "DELETE", + `/notifications/${notificationId}/delete`, + null, + true, + false, + ); + await this.notificationState(userId).update((current) => { + const notification = current?.find((n) => n.id === notificationId); + if (notification) { + notification.deletedDate = new Date(); + } + return current; + }); + } + + async clearState(userId: UserId): Promise { + await this.replaceNotificationState(userId, []); + } + + async refreshNotifications(userId: UserId) { + await this.fetchNotificationsFromApi(userId); + } + + /** + * Helper observable to filter notifications by the notification type and user ids + * Returns EMPTY if no user ids are provided + * @param userIds + * @private + */ + private filteredEndUserNotifications$(userIds: UserId[]) { + if (userIds.length == 0) { + return EMPTY; + } + + return this.notificationService.notifications$.pipe( + filter( + ([{ type }, userId]) => + (type === NotificationType.Notification || + type === NotificationType.NotificationStatus) && + userIds.includes(userId), + ), + ); + } + + /** + * Creates a subscription to listen for end user push notifications and notification status updates. + */ + listenForEndUserNotifications(): Subscription { + return this.authService.authStatuses$ + .pipe( + getLoggedInUserIds, + switchMap((userIds) => this.filteredEndUserNotifications$(userIds)), + concatMap(([notification, userId]) => + this.upsertNotification( + userId, + new NotificationViewData(notification.payload as NotificationViewResponse), + ), + ), + ) + .subscribe(); + } + + /** + * Fetches the notifications from the API and updates the local state + * @param userId + * @private + */ + private async fetchNotificationsFromApi(userId: UserId): Promise { + const res = await this.apiService.send( + "GET", + `/notifications?pageSize=${DEFAULT_NOTIFICATION_PAGE_SIZE}`, + null, + true, + true, + ); + const response = new ListResponse(res, NotificationViewResponse); + + if (response.continuationToken != null) { + this.logService.warning( + `More notifications available, but not fetched. Consider increasing the page size from ${DEFAULT_NOTIFICATION_PAGE_SIZE}`, + ); + } + + const notificationData = response.data.map((n) => new NotificationViewData(n)); + await this.replaceNotificationState(userId, notificationData); + } + + /** + * Replaces the local state with notifications and returns the updated state + * @param userId + * @param notifications + * @private + */ + private replaceNotificationState( + userId: UserId, + notifications: NotificationViewData[], + ): Promise { + return this.notificationState(userId).update(() => notifications); + } + + /** + * Updates the local state adding the new notification or updates an existing one with the same id + * Returns the entire updated notifications state + * @param userId + * @param notification + * @private + */ + private async upsertNotification( + userId: UserId, + notification: NotificationViewData, + ): Promise { + return this.notificationState(userId).update((current) => { + current ??= []; + + const existingIndex = current.findIndex((n) => n.id === notification.id); + + if (existingIndex === -1) { + current.push(notification); + } else { + current[existingIndex] = notification; + } + + return current; + }); + } + + /** + * Returns the local state for notifications + * @param userId + * @private + */ + private notificationState(userId: UserId) { + return this.stateProvider.getUser(userId, NOTIFICATIONS); + } +} diff --git a/libs/vault/src/notifications/state/end-user-notification.state.ts b/libs/common/src/vault/notifications/state/end-user-notification.state.ts similarity index 100% rename from libs/vault/src/notifications/state/end-user-notification.state.ts rename to libs/common/src/vault/notifications/state/end-user-notification.state.ts diff --git a/libs/vault/src/index.ts b/libs/vault/src/index.ts index 0ab85f47252..f3658046a3d 100644 --- a/libs/vault/src/index.ts +++ b/libs/vault/src/index.ts @@ -23,7 +23,6 @@ export * from "./components/add-edit-folder-dialog/add-edit-folder-dialog.compon export * from "./components/carousel"; export * as VaultIcons from "./icons"; -export * from "./notifications"; export * from "./services/vault-nudges.service"; export { DefaultSshImportPromptService } from "./services/default-ssh-import-prompt.service"; diff --git a/libs/vault/src/notifications/index.ts b/libs/vault/src/notifications/index.ts deleted file mode 100644 index 0c9d5c0d16b..00000000000 --- a/libs/vault/src/notifications/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./abstractions/end-user-notification.service"; -export * from "./services/default-end-user-notification.service"; diff --git a/libs/vault/src/notifications/services/default-end-user-notification.service.spec.ts b/libs/vault/src/notifications/services/default-end-user-notification.service.spec.ts deleted file mode 100644 index 1d7b2e5aa19..00000000000 --- a/libs/vault/src/notifications/services/default-end-user-notification.service.spec.ts +++ /dev/null @@ -1,200 +0,0 @@ -import { TestBed } from "@angular/core/testing"; -import { firstValueFrom, of } from "rxjs"; - -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { NotificationsService } from "@bitwarden/common/platform/notifications"; -import { StateProvider } from "@bitwarden/common/platform/state"; -import { NotificationId, UserId } from "@bitwarden/common/types/guid"; -import { DefaultEndUserNotificationService } from "@bitwarden/vault"; - -import { FakeStateProvider, mockAccountServiceWith } from "../../../../common/spec"; -import { NotificationViewResponse } from "../models"; -import { NOTIFICATIONS } from "../state/end-user-notification.state"; - -describe("End User Notification Center Service", () => { - let fakeStateProvider: FakeStateProvider; - - const mockApiSend = jest.fn(); - - let testBed: TestBed; - - beforeEach(async () => { - mockApiSend.mockClear(); - - fakeStateProvider = new FakeStateProvider(mockAccountServiceWith("user-id" as UserId)); - - testBed = TestBed.configureTestingModule({ - imports: [], - providers: [ - DefaultEndUserNotificationService, - { - provide: StateProvider, - useValue: fakeStateProvider, - }, - { - provide: ApiService, - useValue: { - send: mockApiSend, - }, - }, - { - provide: NotificationsService, - useValue: { - notifications$: of(null), - }, - }, - ], - }); - }); - - describe("notifications$", () => { - it("should return notifications from state when not null", async () => { - fakeStateProvider.singleUser.mockFor("user-id" as UserId, NOTIFICATIONS, [ - { - id: "notification-id" as NotificationId, - } as NotificationViewResponse, - ]); - - const { notifications$ } = testBed.inject(DefaultEndUserNotificationService); - - const result = await firstValueFrom(notifications$("user-id" as UserId)); - - expect(result.length).toBe(1); - expect(mockApiSend).not.toHaveBeenCalled(); - }); - - it("should return notifications API when state is null", async () => { - mockApiSend.mockResolvedValue({ - data: [ - { - id: "notification-id", - }, - ] as NotificationViewResponse[], - }); - - fakeStateProvider.singleUser.mockFor("user-id" as UserId, NOTIFICATIONS, null as any); - - const { notifications$ } = testBed.inject(DefaultEndUserNotificationService); - - const result = await firstValueFrom(notifications$("user-id" as UserId)); - - expect(result.length).toBe(1); - expect(mockApiSend).toHaveBeenCalledWith("GET", "/notifications", null, true, true); - }); - - it("should share the same observable for the same user", async () => { - const { notifications$ } = testBed.inject(DefaultEndUserNotificationService); - - const first = notifications$("user-id" as UserId); - const second = notifications$("user-id" as UserId); - - expect(first).toBe(second); - }); - }); - - describe("unreadNotifications$", () => { - it("should return unread notifications from state when read value is null", async () => { - fakeStateProvider.singleUser.mockFor("user-id" as UserId, NOTIFICATIONS, [ - { - id: "notification-id" as NotificationId, - readDate: null as any, - } as NotificationViewResponse, - ]); - - const { unreadNotifications$ } = testBed.inject(DefaultEndUserNotificationService); - - const result = await firstValueFrom(unreadNotifications$("user-id" as UserId)); - - expect(result.length).toBe(1); - expect(mockApiSend).not.toHaveBeenCalled(); - }); - }); - - describe("getNotifications", () => { - it("should call getNotifications returning notifications from API", async () => { - mockApiSend.mockResolvedValue({ - data: [ - { - id: "notification-id", - }, - ] as NotificationViewResponse[], - }); - const service = testBed.inject(DefaultEndUserNotificationService); - - await service.getNotifications("user-id" as UserId); - - expect(mockApiSend).toHaveBeenCalledWith("GET", "/notifications", null, true, true); - }); - }); - it("should update local state when notifications are updated", async () => { - mockApiSend.mockResolvedValue({ - data: [ - { - id: "notification-id", - }, - ] as NotificationViewResponse[], - }); - - const mock = fakeStateProvider.singleUser.mockFor( - "user-id" as UserId, - NOTIFICATIONS, - null as any, - ); - - const service = testBed.inject(DefaultEndUserNotificationService); - - await service.getNotifications("user-id" as UserId); - - expect(mock.nextMock).toHaveBeenCalledWith([ - expect.objectContaining({ - id: "notification-id" as NotificationId, - } as NotificationViewResponse), - ]); - }); - - describe("clear", () => { - it("should clear the local notification state for the user", async () => { - const mock = fakeStateProvider.singleUser.mockFor("user-id" as UserId, NOTIFICATIONS, [ - { - id: "notification-id" as NotificationId, - } as NotificationViewResponse, - ]); - - const service = testBed.inject(DefaultEndUserNotificationService); - - await service.clearState("user-id" as UserId); - - expect(mock.nextMock).toHaveBeenCalledWith([]); - }); - }); - - describe("markAsDeleted", () => { - it("should send an API request to mark the notification as deleted", async () => { - const service = testBed.inject(DefaultEndUserNotificationService); - - await service.markAsDeleted("notification-id" as NotificationId, "user-id" as UserId); - expect(mockApiSend).toHaveBeenCalledWith( - "DELETE", - "/notifications/notification-id/delete", - null, - true, - false, - ); - }); - }); - - describe("markAsRead", () => { - it("should send an API request to mark the notification as read", async () => { - const service = testBed.inject(DefaultEndUserNotificationService); - - await service.markAsRead("notification-id" as NotificationId, "user-id" as UserId); - expect(mockApiSend).toHaveBeenCalledWith( - "PATCH", - "/notifications/notification-id/read", - null, - true, - false, - ); - }); - }); -}); diff --git a/libs/vault/src/notifications/services/default-end-user-notification.service.ts b/libs/vault/src/notifications/services/default-end-user-notification.service.ts deleted file mode 100644 index 471ed0e5856..00000000000 --- a/libs/vault/src/notifications/services/default-end-user-notification.service.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { Injectable } from "@angular/core"; -import { concatMap, filter, map, Observable, switchMap } from "rxjs"; - -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { NotificationType } from "@bitwarden/common/enums"; -import { ListResponse } from "@bitwarden/common/models/response/list.response"; -import { NotificationsService } from "@bitwarden/common/platform/notifications"; -import { StateProvider } from "@bitwarden/common/platform/state"; -import { UserId } from "@bitwarden/common/types/guid"; -import { - filterOutNullish, - perUserCache$, -} from "@bitwarden/common/vault/utils/observable-utilities"; - -import { EndUserNotificationService } from "../abstractions/end-user-notification.service"; -import { NotificationView, NotificationViewData, NotificationViewResponse } from "../models"; -import { NOTIFICATIONS } from "../state/end-user-notification.state"; - -/** - * A service for retrieving and managing notifications for end users. - */ -@Injectable({ - providedIn: "root", -}) -export class DefaultEndUserNotificationService implements EndUserNotificationService { - constructor( - private stateProvider: StateProvider, - private apiService: ApiService, - private defaultNotifications: NotificationsService, - ) { - this.defaultNotifications.notifications$ - .pipe( - filter( - ([notification]) => - notification.type === NotificationType.Notification || - notification.type === NotificationType.NotificationStatus, - ), - concatMap(([notification, userId]) => - this.updateNotificationState(userId, [ - new NotificationViewData(notification.payload as NotificationViewResponse), - ]), - ), - ) - .subscribe(); - } - - notifications$ = perUserCache$((userId: UserId): Observable => { - return this.notificationState(userId).state$.pipe( - switchMap(async (notifications) => { - if (notifications == null) { - await this.fetchNotificationsFromApi(userId); - } - return notifications; - }), - filterOutNullish(), - map((notifications) => - notifications.map((notification) => new NotificationView(notification)), - ), - ); - }); - - unreadNotifications$ = perUserCache$((userId: UserId): Observable => { - return this.notifications$(userId).pipe( - map((notifications) => notifications.filter((notification) => notification.readDate == null)), - ); - }); - - async markAsRead(notificationId: any, userId: UserId): Promise { - await this.apiService.send("PATCH", `/notifications/${notificationId}/read`, null, true, false); - await this.getNotifications(userId); - } - - async markAsDeleted(notificationId: any, userId: UserId): Promise { - await this.apiService.send( - "DELETE", - `/notifications/${notificationId}/delete`, - null, - true, - false, - ); - await this.getNotifications(userId); - } - - async clearState(userId: UserId): Promise { - await this.updateNotificationState(userId, []); - } - - async getNotifications(userId: UserId) { - await this.fetchNotificationsFromApi(userId); - } - - /** - * Fetches the notifications from the API and updates the local state - * @param userId - * @private - */ - private async fetchNotificationsFromApi(userId: UserId): Promise { - const res = await this.apiService.send("GET", "/notifications", null, true, true); - const response = new ListResponse(res, NotificationViewResponse); - const notificationData = response.data.map((n) => new NotificationView(n)); - await this.updateNotificationState(userId, notificationData); - } - - /** - * Updates the local state with notifications and returns the updated state - * @param userId - * @param notifications - * @private - */ - private updateNotificationState( - userId: UserId, - notifications: NotificationViewData[], - ): Promise { - return this.notificationState(userId).update(() => notifications); - } - - /** - * Returns the local state for notifications - * @param userId - * @private - */ - private notificationState(userId: UserId) { - return this.stateProvider.getUser(userId, NOTIFICATIONS); - } -} From 3a8045d7d04ac09f430cd1bf411438ebd4ba4810 Mon Sep 17 00:00:00 2001 From: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> Date: Mon, 21 Apr 2025 14:08:09 -0500 Subject: [PATCH 024/499] [PM-19215] Fix Firefox extension biometric unlock autoprompt (#14254) * Remove restriction from account security component * Add the ability to manage pop-out to the LockComponentService * Have the Firefox extension pop-out on biometric auto-prompt unlock --- .../settings/account-security.component.html | 2 +- .../settings/account-security.component.ts | 7 --- .../extension-lock-component.service.spec.ts | 58 +++++++++++++++++++ .../extension-lock-component.service.ts | 14 +++++ .../desktop-lock-component.service.spec.ts | 16 +++++ .../desktop-lock-component.service.ts | 8 +++ .../web-lock-component.service.spec.ts | 16 +++++ .../services/web-lock-component.service.ts | 8 +++ .../src/lock/components/lock.component.html | 6 +- .../src/lock/components/lock.component.ts | 45 +++++++++----- .../lock/services/lock-component.service.ts | 12 ++++ 11 files changed, 166 insertions(+), 26 deletions(-) diff --git a/apps/browser/src/auth/popup/settings/account-security.component.html b/apps/browser/src/auth/popup/settings/account-security.component.html index b8252aa6e13..ebf79af644c 100644 --- a/apps/browser/src/auth/popup/settings/account-security.component.html +++ b/apps/browser/src/auth/popup/settings/account-security.component.html @@ -23,7 +23,7 @@ = of(true); form = this.formBuilder.group({ @@ -147,11 +145,6 @@ export class AccountSecurityComponent implements OnInit, OnDestroy { ) {} async ngOnInit() { - // Firefox popup closes when unfocused by biometrics, blocking all unlock methods - if (this.platformUtilsService.getDevice() === DeviceType.FirefoxExtension) { - this.showAutoPrompt = false; - } - const hasMasterPassword = await this.userVerificationService.hasMasterPassword(); this.showMasterPasswordOnClientRestartOption = hasMasterPassword; const maximumVaultTimeoutPolicy = this.accountService.activeAccount$.pipe( diff --git a/apps/browser/src/key-management/lock/services/extension-lock-component.service.spec.ts b/apps/browser/src/key-management/lock/services/extension-lock-component.service.spec.ts index 9afc723825c..ac5331d3627 100644 --- a/apps/browser/src/key-management/lock/services/extension-lock-component.service.spec.ts +++ b/apps/browser/src/key-management/lock/services/extension-lock-component.service.spec.ts @@ -17,6 +17,8 @@ import { } from "@bitwarden/key-management"; import { UnlockOptions } from "@bitwarden/key-management-ui"; +import { BrowserApi } from "../../../platform/browser/browser-api"; +import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils"; import { BrowserRouterService } from "../../../platform/popup/services/browser-router.service"; import { ExtensionLockComponentService } from "./extension-lock-component.service"; @@ -117,6 +119,62 @@ describe("ExtensionLockComponentService", () => { }); }); + describe("popOutBrowserExtension", () => { + let openPopoutSpy: jest.SpyInstance; + beforeEach(() => { + jest.resetAllMocks(); + openPopoutSpy = jest + .spyOn(BrowserPopupUtils, "openCurrentPagePopout") + .mockResolvedValue(undefined); + }); + + it("opens pop-out when the current window is neither a pop-out nor a sidebar", async () => { + jest.spyOn(BrowserPopupUtils, "inPopout").mockReturnValue(false); + jest.spyOn(BrowserPopupUtils, "inSidebar").mockReturnValue(false); + + await service.popOutBrowserExtension(); + + expect(openPopoutSpy).toHaveBeenCalledWith(global.window); + }); + + test.each([ + [true, false], + [false, true], + [true, true], + ])("should not open pop-out under other conditions.", async (inPopout, inSidebar) => { + jest.spyOn(BrowserPopupUtils, "inPopout").mockReturnValue(inPopout); + jest.spyOn(BrowserPopupUtils, "inSidebar").mockReturnValue(inSidebar); + + await service.popOutBrowserExtension(); + + expect(openPopoutSpy).not.toHaveBeenCalled(); + }); + }); + + describe("closeBrowserExtensionPopout", () => { + let closePopupSpy: jest.SpyInstance; + beforeEach(() => { + jest.resetAllMocks(); + closePopupSpy = jest.spyOn(BrowserApi, "closePopup").mockReturnValue(); + }); + + it("closes pop-out when in pop-out", () => { + jest.spyOn(BrowserPopupUtils, "inPopout").mockReturnValue(true); + + service.closeBrowserExtensionPopout(); + + expect(closePopupSpy).toHaveBeenCalledWith(global.window); + }); + + it("doesn't close pop-out when not in pop-out", () => { + jest.spyOn(BrowserPopupUtils, "inPopout").mockReturnValue(false); + + service.closeBrowserExtensionPopout(); + + expect(closePopupSpy).not.toHaveBeenCalled(); + }); + }); + describe("isWindowVisible", () => { it("throws an error", async () => { await expect(service.isWindowVisible()).rejects.toThrow("Method not implemented."); diff --git a/apps/browser/src/key-management/lock/services/extension-lock-component.service.ts b/apps/browser/src/key-management/lock/services/extension-lock-component.service.ts index 09a6f890e60..6ee1fc5175f 100644 --- a/apps/browser/src/key-management/lock/services/extension-lock-component.service.ts +++ b/apps/browser/src/key-management/lock/services/extension-lock-component.service.ts @@ -14,6 +14,8 @@ import { import { LockComponentService, UnlockOptions } from "@bitwarden/key-management-ui"; import { BiometricErrors, BiometricErrorTypes } from "../../../models/biometricErrors"; +import { BrowserApi } from "../../../platform/browser/browser-api"; +import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils"; import { BrowserRouterService } from "../../../platform/popup/services/browser-router.service"; export class ExtensionLockComponentService implements LockComponentService { @@ -37,6 +39,18 @@ export class ExtensionLockComponentService implements LockComponentService { return biometricsError.description; } + async popOutBrowserExtension(): Promise { + if (!BrowserPopupUtils.inPopout(global.window) && !BrowserPopupUtils.inSidebar(global.window)) { + await BrowserPopupUtils.openCurrentPagePopout(global.window); + } + } + + closeBrowserExtensionPopout(): void { + if (BrowserPopupUtils.inPopout(global.window)) { + BrowserApi.closePopup(global.window); + } + } + async isWindowVisible(): Promise { throw new Error("Method not implemented."); } diff --git a/apps/desktop/src/key-management/lock/services/desktop-lock-component.service.spec.ts b/apps/desktop/src/key-management/lock/services/desktop-lock-component.service.spec.ts index 6bfbc803e87..6772af4f905 100644 --- a/apps/desktop/src/key-management/lock/services/desktop-lock-component.service.spec.ts +++ b/apps/desktop/src/key-management/lock/services/desktop-lock-component.service.spec.ts @@ -104,6 +104,22 @@ describe("DesktopLockComponentService", () => { }); }); + describe("popOutBrowserExtension", () => { + it("throws platform not supported error", () => { + expect(() => service.popOutBrowserExtension()).toThrow( + "Method not supported on this platform.", + ); + }); + }); + + describe("closeBrowserExtensionPopout", () => { + it("throws platform not supported error", () => { + expect(() => service.closeBrowserExtensionPopout()).toThrow( + "Method not supported on this platform.", + ); + }); + }); + describe("isWindowVisible", () => { it("returns the window visibility", async () => { isWindowVisibleMock.mockReturnValue(true); diff --git a/apps/desktop/src/key-management/lock/services/desktop-lock-component.service.ts b/apps/desktop/src/key-management/lock/services/desktop-lock-component.service.ts index 72df9336ea2..5cb3803930d 100644 --- a/apps/desktop/src/key-management/lock/services/desktop-lock-component.service.ts +++ b/apps/desktop/src/key-management/lock/services/desktop-lock-component.service.ts @@ -27,6 +27,14 @@ export class DesktopLockComponentService implements LockComponentService { return null; } + popOutBrowserExtension(): Promise { + throw new Error("Method not supported on this platform."); + } + + closeBrowserExtensionPopout(): void { + throw new Error("Method not supported on this platform."); + } + async isWindowVisible(): Promise { return ipc.platform.isWindowVisible(); } diff --git a/apps/web/src/app/key-management/lock/services/web-lock-component.service.spec.ts b/apps/web/src/app/key-management/lock/services/web-lock-component.service.spec.ts index 3c941fe24c7..9e993259830 100644 --- a/apps/web/src/app/key-management/lock/services/web-lock-component.service.spec.ts +++ b/apps/web/src/app/key-management/lock/services/web-lock-component.service.spec.ts @@ -52,6 +52,22 @@ describe("WebLockComponentService", () => { }); }); + describe("popOutBrowserExtension", () => { + it("throws platform not supported error", () => { + expect(() => service.popOutBrowserExtension()).toThrow( + "Method not supported on this platform.", + ); + }); + }); + + describe("closeBrowserExtensionPopout", () => { + it("throws platform not supported error", () => { + expect(() => service.closeBrowserExtensionPopout()).toThrow( + "Method not supported on this platform.", + ); + }); + }); + describe("isWindowVisible", () => { it("throws an error", async () => { await expect(service.isWindowVisible()).rejects.toThrow("Method not implemented."); diff --git a/apps/web/src/app/key-management/lock/services/web-lock-component.service.ts b/apps/web/src/app/key-management/lock/services/web-lock-component.service.ts index dd9f5138dba..ea038ca2c67 100644 --- a/apps/web/src/app/key-management/lock/services/web-lock-component.service.ts +++ b/apps/web/src/app/key-management/lock/services/web-lock-component.service.ts @@ -24,6 +24,14 @@ export class WebLockComponentService implements LockComponentService { return null; } + popOutBrowserExtension(): Promise { + throw new Error("Method not supported on this platform."); + } + + closeBrowserExtensionPopout(): void { + throw new Error("Method not supported on this platform."); + } + async isWindowVisible(): Promise { throw new Error("Method not implemented."); } diff --git a/libs/key-management-ui/src/lock/components/lock.component.html b/libs/key-management-ui/src/lock/components/lock.component.html index 437e29447e2..efc7fb26a2f 100644 --- a/libs/key-management-ui/src/lock/components/lock.component.html +++ b/libs/key-management-ui/src/lock/components/lock.component.html @@ -1,10 +1,10 @@ - -
+ +
- + +
+
+
diff --git a/apps/browser/src/platform/popup/layout/popup-tab-navigation.component.ts b/apps/browser/src/platform/popup/layout/popup-tab-navigation.component.ts index e01b4efd71b..f4b82dc56fc 100644 --- a/apps/browser/src/platform/popup/layout/popup-tab-navigation.component.ts +++ b/apps/browser/src/platform/popup/layout/popup-tab-navigation.component.ts @@ -1,10 +1,19 @@ import { CommonModule } from "@angular/common"; -import { Component } from "@angular/core"; +import { Component, Input } from "@angular/core"; import { RouterModule } from "@angular/router"; import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LinkModule } from "@bitwarden/components"; +export type NavButton = { + label: string; + page: string; + iconKey: string; + iconKeyActive: string; + showBerry?: boolean; +}; + @Component({ selector: "popup-tab-navigation", templateUrl: "popup-tab-navigation.component.html", @@ -15,30 +24,12 @@ import { LinkModule } from "@bitwarden/components"; }, }) export class PopupTabNavigationComponent { - navButtons = [ - { - label: "vault", - page: "/tabs/vault", - iconKey: "lock", - iconKeyActive: "lock-f", - }, - { - label: "generator", - page: "/tabs/generator", - iconKey: "generate", - iconKeyActive: "generate-f", - }, - { - label: "send", - page: "/tabs/send", - iconKey: "send", - iconKeyActive: "send-f", - }, - { - label: "settings", - page: "/tabs/settings", - iconKey: "cog", - iconKeyActive: "cog-f", - }, - ]; + @Input() navButtons: NavButton[] = []; + + constructor(private i18nService: I18nService) {} + + buttonTitle(navButton: NavButton) { + const labelText = this.i18nService.t(navButton.label); + return navButton.showBerry ? this.i18nService.t("labelWithNotification", labelText) : labelText; + } } diff --git a/apps/browser/src/popup/tabs-v2.component.html b/apps/browser/src/popup/tabs-v2.component.html new file mode 100644 index 00000000000..bde3aaa3d31 --- /dev/null +++ b/apps/browser/src/popup/tabs-v2.component.html @@ -0,0 +1,3 @@ + + + diff --git a/apps/browser/src/popup/tabs-v2.component.ts b/apps/browser/src/popup/tabs-v2.component.ts index 4cdb8fc029d..1392dc565ab 100644 --- a/apps/browser/src/popup/tabs-v2.component.ts +++ b/apps/browser/src/popup/tabs-v2.component.ts @@ -1,11 +1,53 @@ import { Component } from "@angular/core"; +import { combineLatest, map } from "rxjs"; + +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { HasNudgeService } from "@bitwarden/vault"; @Component({ selector: "app-tabs-v2", - template: ` - - - - `, + templateUrl: "./tabs-v2.component.html", + providers: [HasNudgeService], }) -export class TabsV2Component {} +export class TabsV2Component { + constructor( + private readonly hasNudgeService: HasNudgeService, + private readonly configService: ConfigService, + ) {} + + protected navButtons$ = combineLatest([ + this.configService.getFeatureFlag$(FeatureFlag.PM8851_BrowserOnboardingNudge), + this.hasNudgeService.shouldShowNudge$(), + ]).pipe( + map(([onboardingFeatureEnabled, showNudge]) => { + return [ + { + label: "vault", + page: "/tabs/vault", + iconKey: "lock", + iconKeyActive: "lock-f", + }, + { + label: "generator", + page: "/tabs/generator", + iconKey: "generate", + iconKeyActive: "generate-f", + }, + { + label: "send", + page: "/tabs/send", + iconKey: "send", + iconKeyActive: "send-f", + }, + { + label: "settings", + page: "/tabs/settings", + iconKey: "cog", + iconKeyActive: "cog-f", + showBerry: onboardingFeatureEnabled && showNudge, + }, + ]; + }), + ); +} diff --git a/libs/vault/src/index.ts b/libs/vault/src/index.ts index f3658046a3d..655b536de64 100644 --- a/libs/vault/src/index.ts +++ b/libs/vault/src/index.ts @@ -24,6 +24,7 @@ export * from "./components/carousel"; export * as VaultIcons from "./icons"; export * from "./services/vault-nudges.service"; +export * from "./services/custom-nudges-services"; export { DefaultSshImportPromptService } from "./services/default-ssh-import-prompt.service"; export { SshImportPromptService } from "./services/ssh-import-prompt.service"; diff --git a/libs/vault/src/services/custom-nudges-services/has-nudge.service.ts b/libs/vault/src/services/custom-nudges-services/has-nudge.service.ts new file mode 100644 index 00000000000..b1f319451e6 --- /dev/null +++ b/libs/vault/src/services/custom-nudges-services/has-nudge.service.ts @@ -0,0 +1,47 @@ +import { inject, Injectable } from "@angular/core"; +import { combineLatest, distinctUntilChanged, map, Observable, of, switchMap } from "rxjs"; + +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { UserId } from "@bitwarden/common/types/guid"; + +import { DefaultSingleNudgeService } from "../default-single-nudge.service"; +import { VaultNudgeType } from "../vault-nudges.service"; + +/** + * Custom Nudge Service used for showing if the user has any existing nudge in the Vault. + */ +@Injectable({ + providedIn: "root", +}) +export class HasNudgeService extends DefaultSingleNudgeService { + private accountService = inject(AccountService); + + private nudgeTypes: VaultNudgeType[] = [ + VaultNudgeType.HasVaultItems, + VaultNudgeType.IntroCarouselDismissal, + // add additional nudge types here as needed + ]; + + /** + * Returns an observable that emits true if any of the provided nudge types are present + */ + shouldShowNudge$(): Observable { + return this.accountService.activeAccount$.pipe( + switchMap((activeAccount) => { + const userId: UserId | undefined = activeAccount?.id; + if (!userId) { + return of(false); + } + + const nudgeObservables: Observable[] = this.nudgeTypes.map((nudge) => + super.shouldShowNudge$(nudge, userId), + ); + + return combineLatest(nudgeObservables).pipe( + map((nudgeStates) => nudgeStates.some((state) => state)), + distinctUntilChanged(), + ); + }), + ); + } +} diff --git a/libs/vault/src/services/custom-nudges-services/index.ts b/libs/vault/src/services/custom-nudges-services/index.ts index 84409eb35ae..dd343e47d75 100644 --- a/libs/vault/src/services/custom-nudges-services/index.ts +++ b/libs/vault/src/services/custom-nudges-services/index.ts @@ -1 +1,2 @@ export * from "./has-items-nudge.service"; +export * from "./has-nudge.service"; From c08888bbd963e70784d58053e22da6f9b09a1442 Mon Sep 17 00:00:00 2001 From: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> Date: Tue, 22 Apr 2025 21:51:14 +0200 Subject: [PATCH 034/499] Move feature flags for Data Insights and Reporting (#14375) Co-authored-by: Daniel James Smith --- libs/common/src/enums/feature-flag.enum.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index 08906c792fb..33843932382 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -35,6 +35,10 @@ export enum FeatureFlag { PM18794_ProviderPaymentMethod = "pm-18794-provider-payment-method", PM17772_AdminInitiatedSponsorships = "pm-17772-admin-initiated-sponsorships", + /* Data Insights and Reporting */ + CriticalApps = "pm-14466-risk-insights-critical-application", + EnableRiskInsightsNotifications = "enable-risk-insights-notifications", + /* Key Management */ PrivateKeyRegeneration = "pm-12241-private-key-regeneration", UserKeyRotationV2 = "userkey-rotation-v2", @@ -43,8 +47,6 @@ export enum FeatureFlag { /* Tools */ ItemShare = "item-share", - CriticalApps = "pm-14466-risk-insights-critical-application", - EnableRiskInsightsNotifications = "enable-risk-insights-notifications", DesktopSendUIRefresh = "desktop-send-ui-refresh", /* Vault */ @@ -92,10 +94,12 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.UseTreeWalkerApiForPageDetailsCollection]: FALSE, [FeatureFlag.MacOsNativeCredentialSync]: FALSE, - /* Tools */ - [FeatureFlag.ItemShare]: FALSE, + /* Data Insights and Reporting */ [FeatureFlag.CriticalApps]: FALSE, [FeatureFlag.EnableRiskInsightsNotifications]: FALSE, + + /* Tools */ + [FeatureFlag.ItemShare]: FALSE, [FeatureFlag.DesktopSendUIRefresh]: FALSE, /* Vault */ From 95d0fb9012fcafbd6830fa5ae9170c35a0962f54 Mon Sep 17 00:00:00 2001 From: Daniel Riera Date: Tue, 22 Apr 2025 16:35:44 -0400 Subject: [PATCH 035/499] PM-20490 add of to mitigate return-type annotation (#14377) --- .../src/autofill/background/notification.background.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/browser/src/autofill/background/notification.background.ts b/apps/browser/src/autofill/background/notification.background.ts index 6589252d94b..00d24300d78 100644 --- a/apps/browser/src/autofill/background/notification.background.ts +++ b/apps/browser/src/autofill/background/notification.background.ts @@ -1,6 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { firstValueFrom, switchMap, map } from "rxjs"; +import { firstValueFrom, switchMap, map, of } from "rxjs"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; @@ -780,7 +780,7 @@ export default class NotificationBackground { this.taskService.tasksEnabled$(userId).pipe( switchMap((tasksEnabled) => { if (!tasksEnabled) { - return []; + return of([]); } return this.taskService From 71e720e94513b7d36f80223387719338fa5406f2 Mon Sep 17 00:00:00 2001 From: Alec Rippberger <127791530+alec-livefront@users.noreply.github.com> Date: Tue, 22 Apr 2025 23:09:07 -0500 Subject: [PATCH 036/499] fix(auth): clarify 2FA security key verification text Updates user interface text to improve clarity when prompting for security key verification during two-factor authentication. Ref: PM-20055 --- apps/browser/src/_locales/en/messages.json | 3 +++ apps/desktop/src/locales/en/messages.json | 3 +++ apps/web/src/connectors/webauthn-fallback.ts | 2 +- apps/web/src/locales/en/messages.json | 3 +++ .../src/angular/two-factor-auth/two-factor-auth.component.ts | 2 +- 5 files changed, 11 insertions(+), 2 deletions(-) diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 73c64ba7fd4..462af12a352 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -886,6 +886,9 @@ "followTheStepsBelowToFinishLoggingIn": { "message": "Follow the steps below to finish logging in." }, + "followTheStepsBelowToFinishLoggingInWithSecurityKey": { + "message": "Follow the steps below to finish logging in with your security key." + }, "restartRegistration": { "message": "Restart registration" }, diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index 8850cbe5a3f..6097543e50a 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -3212,6 +3212,9 @@ "followTheStepsBelowToFinishLoggingIn": { "message": "Follow the steps below to finish logging in." }, + "followTheStepsBelowToFinishLoggingInWithSecurityKey": { + "message": "Follow the steps below to finish logging in with your security key." + }, "launchDuo": { "message": "Launch Duo in Browser" }, diff --git a/apps/web/src/connectors/webauthn-fallback.ts b/apps/web/src/connectors/webauthn-fallback.ts index 3561f922e03..43be5733973 100644 --- a/apps/web/src/connectors/webauthn-fallback.ts +++ b/apps/web/src/connectors/webauthn-fallback.ts @@ -86,7 +86,7 @@ document.addEventListener("DOMContentLoaded", async () => { titleForLargerScreens.innerText = localeService.t("verifyYourIdentity"); const subtitle = document.getElementById("subtitle"); - subtitle.innerText = localeService.t("followTheStepsBelowToFinishLoggingIn"); + subtitle.innerText = localeService.t("followTheStepsBelowToFinishLoggingInWithSecurityKey"); }); function start() { diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 56a98a661ef..05d29071731 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -7280,6 +7280,9 @@ "followTheStepsBelowToFinishLoggingIn": { "message": "Follow the steps below to finish logging in." }, + "followTheStepsBelowToFinishLoggingInWithSecurityKey": { + "message": "Follow the steps below to finish logging in with your security key." + }, "launchDuo": { "message": "Launch Duo" }, diff --git a/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.ts b/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.ts index 6cdf42b76da..aed9f9f07a5 100644 --- a/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.ts +++ b/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.ts @@ -362,7 +362,7 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy { break; case TwoFactorProviderType.WebAuthn: this.anonLayoutWrapperDataService.setAnonLayoutWrapperData({ - pageSubtitle: this.i18nService.t("followTheStepsBelowToFinishLoggingIn"), + pageSubtitle: this.i18nService.t("followTheStepsBelowToFinishLoggingInWithSecurityKey"), pageIcon: TwoFactorAuthWebAuthnIcon, }); break; From 60fe8fa7b00dbb27a419e926e7d3dc4e376f92a4 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Wed, 23 Apr 2025 14:21:45 +0200 Subject: [PATCH 037/499] Add comments to send service to make it easier to follow (#14389) --- libs/common/src/tools/send/services/send.service.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/libs/common/src/tools/send/services/send.service.ts b/libs/common/src/tools/send/services/send.service.ts index 8d6e62e3b8c..cefd9942d29 100644 --- a/libs/common/src/tools/send/services/send.service.ts +++ b/libs/common/src/tools/send/services/send.service.ts @@ -50,7 +50,7 @@ export class SendService implements InternalSendServiceAbstraction { model: SendView, file: File | ArrayBuffer, password: string, - key?: SymmetricCryptoKey, + userKey?: SymmetricCryptoKey, ): Promise<[Send, EncArrayBuffer]> { let fileData: EncArrayBuffer = null; const send = new Send(); @@ -62,15 +62,19 @@ export class SendService implements InternalSendServiceAbstraction { send.deletionDate = model.deletionDate; send.expirationDate = model.expirationDate; if (model.key == null) { + // Sends use a seed, stored in the URL fragment. This seed is used to derive the key that is used for encryption. const key = await this.keyGenerationService.createKeyWithPurpose( 128, this.sendKeyPurpose, this.sendKeySalt, ); + // key.material is the seed that can be used to re-derive the key model.key = key.material; model.cryptoKey = key.derivedKey; } if (password != null) { + // Note: Despite being called key, the passwordKey is not used for encryption. + // It is used as a static proof that the client knows the password, and has the encryption key. const passwordKey = await this.keyGenerationService.deriveKeyFromPassword( password, model.key, @@ -78,11 +82,11 @@ export class SendService implements InternalSendServiceAbstraction { ); send.password = passwordKey.keyB64; } - if (key == null) { - key = await this.keyService.getUserKey(); + if (userKey == null) { + userKey = await this.keyService.getUserKey(); } // Key is not a SymmetricCryptoKey, but key material used to derive the cryptoKey - send.key = await this.encryptService.encrypt(model.key, key); + send.key = await this.encryptService.encrypt(model.key, userKey); send.name = await this.encryptService.encrypt(model.name, model.cryptoKey); send.notes = await this.encryptService.encrypt(model.notes, model.cryptoKey); if (send.type === SendType.Text) { From 8e1dfb7d214f3f457f688b5f693830d9355853d2 Mon Sep 17 00:00:00 2001 From: Daniel Riera Date: Wed, 23 Apr 2025 09:59:05 -0400 Subject: [PATCH 038/499] PM-19281 localize relevant notification bar text (#14327) * PM-19281 localize todo text * update storybook --- .../content/components/cipher/cipher-action.ts | 8 ++++---- .../content/components/cipher/cipher-item.ts | 4 +++- .../ciphers/cipher-action.lit-stories.ts | 1 + .../lit-stories/notification/body.lit-stories.ts | 1 + .../content/components/notification/body.ts | 3 +++ .../content/components/notification/button-row.ts | 14 ++++++++------ .../content/components/notification/container.ts | 1 + .../content/components/notification/footer.ts | 1 + apps/browser/src/autofill/notification/bar.ts | 7 +++++-- 9 files changed, 27 insertions(+), 13 deletions(-) diff --git a/apps/browser/src/autofill/content/components/cipher/cipher-action.ts b/apps/browser/src/autofill/content/components/cipher/cipher-action.ts index aaa4b11d8a2..85698c87c67 100644 --- a/apps/browser/src/autofill/content/components/cipher/cipher-action.ts +++ b/apps/browser/src/autofill/content/components/cipher/cipher-action.ts @@ -8,24 +8,24 @@ export function CipherAction({ handleAction = () => { /* no-op */ }, + i18n, notificationType, theme, }: { handleAction?: (e: Event) => void; + i18n: { [key: string]: string }; notificationType: typeof NotificationTypes.Change | typeof NotificationTypes.Add; theme: Theme; }) { return notificationType === NotificationTypes.Change ? BadgeButton({ buttonAction: handleAction, - // @TODO localize - buttonText: "Update", + buttonText: i18n.notificationUpdate, theme, }) : EditButton({ buttonAction: handleAction, - // @TODO localize - buttonText: "Edit", + buttonText: i18n.notificationEdit, theme, }); } diff --git a/apps/browser/src/autofill/content/components/cipher/cipher-item.ts b/apps/browser/src/autofill/content/components/cipher/cipher-item.ts index 96b44d2c0cc..8ab29860f3b 100644 --- a/apps/browser/src/autofill/content/components/cipher/cipher-item.ts +++ b/apps/browser/src/autofill/content/components/cipher/cipher-item.ts @@ -19,11 +19,13 @@ const cipherIconWidth = "24px"; export function CipherItem({ cipher, handleAction, + i18n, notificationType, theme = ThemeTypes.Light, }: { cipher: NotificationCipherData; handleAction?: (e: Event) => void; + i18n: { [key: string]: string }; notificationType?: NotificationType; theme: Theme; }) { @@ -34,7 +36,7 @@ export function CipherItem({ if (notificationType === NotificationTypes.Change || notificationType === NotificationTypes.Add) { cipherActionButton = html`
- ${CipherAction({ handleAction, notificationType, theme })} + ${CipherAction({ handleAction, i18n, notificationType, theme })}
`; } diff --git a/apps/browser/src/autofill/content/components/lit-stories/ciphers/cipher-action.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/ciphers/cipher-action.lit-stories.ts index e597cddabe6..dd1ff816f06 100644 --- a/apps/browser/src/autofill/content/components/lit-stories/ciphers/cipher-action.lit-stories.ts +++ b/apps/browser/src/autofill/content/components/lit-stories/ciphers/cipher-action.lit-stories.ts @@ -7,6 +7,7 @@ import { CipherAction } from "../../cipher/cipher-action"; type Args = { handleAction?: (e: Event) => void; + i18n: { [key: string]: string }; notificationType: typeof NotificationTypes.Change | typeof NotificationTypes.Add; theme: Theme; }; diff --git a/apps/browser/src/autofill/content/components/lit-stories/notification/body.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/notification/body.lit-stories.ts index 32b4170d1da..13e2322a9f2 100644 --- a/apps/browser/src/autofill/content/components/lit-stories/notification/body.lit-stories.ts +++ b/apps/browser/src/autofill/content/components/lit-stories/notification/body.lit-stories.ts @@ -10,6 +10,7 @@ import { NotificationBody } from "../../notification/body"; type Args = { ciphers: NotificationCipherData[]; + i18n: { [key: string]: string }; notificationType: NotificationType; theme: Theme; handleEditOrUpdateAction: (e: Event) => void; diff --git a/apps/browser/src/autofill/content/components/notification/body.ts b/apps/browser/src/autofill/content/components/notification/body.ts index 66b580bde43..cc0fa359303 100644 --- a/apps/browser/src/autofill/content/components/notification/body.ts +++ b/apps/browser/src/autofill/content/components/notification/body.ts @@ -17,12 +17,14 @@ const { css } = createEmotion({ export function NotificationBody({ ciphers = [], + i18n, notificationType, theme = ThemeTypes.Light, handleEditOrUpdateAction, }: { ciphers?: NotificationCipherData[]; customClasses?: string[]; + i18n: { [key: string]: string }; notificationType?: NotificationType; theme: Theme; handleEditOrUpdateAction: (e: Event) => void; @@ -37,6 +39,7 @@ export function NotificationBody({ theme, children: CipherItem({ cipher, + i18n, notificationType, theme, handleAction: handleEditOrUpdateAction, diff --git a/apps/browser/src/autofill/content/components/notification/button-row.ts b/apps/browser/src/autofill/content/components/notification/button-row.ts index 8661f5957e1..3834da4269d 100644 --- a/apps/browser/src/autofill/content/components/notification/button-row.ts +++ b/apps/browser/src/autofill/content/components/notification/button-row.ts @@ -22,17 +22,19 @@ function getVaultIconByProductTier(productTierType?: ProductTierType): Option["i } export type NotificationButtonRowProps = { - theme: Theme; + folders?: FolderView[]; + i18n: { [key: string]: string }; + organizations?: OrgView[]; primaryButton: { text: string; handlePrimaryButtonClick: (args: any) => void; }; - folders?: FolderView[]; - organizations?: OrgView[]; + theme: Theme; }; export function NotificationButtonRow({ folders, + i18n, organizations, primaryButton, theme, @@ -40,7 +42,7 @@ export function NotificationButtonRow({ const currentUserVaultOption: Option = { icon: User, default: true, - text: "My vault", // @TODO localize + text: i18n.myVault, value: "0", }; const organizationOptions: Option[] = organizations?.length @@ -84,7 +86,7 @@ export function NotificationButtonRow({ ? [ { id: "organization", - label: "Vault", // @TODO localize + label: i18n.vault, options: organizationOptions, }, ] @@ -93,7 +95,7 @@ export function NotificationButtonRow({ ? [ { id: "folder", - label: "Folder", // @TODO localize + label: i18n.folder, options: folderOptions, }, ] diff --git a/apps/browser/src/autofill/content/components/notification/container.ts b/apps/browser/src/autofill/content/components/notification/container.ts index c29f58e116b..e1d098e3b09 100644 --- a/apps/browser/src/autofill/content/components/notification/container.ts +++ b/apps/browser/src/autofill/content/components/notification/container.ts @@ -59,6 +59,7 @@ export function NotificationContainer({ ciphers, notificationType: type, theme, + i18n, }) : null} ${NotificationFooter({ diff --git a/apps/browser/src/autofill/content/components/notification/footer.ts b/apps/browser/src/autofill/content/components/notification/footer.ts index 8ed69a96ad9..58a87ebc678 100644 --- a/apps/browser/src/autofill/content/components/notification/footer.ts +++ b/apps/browser/src/autofill/content/components/notification/footer.ts @@ -38,6 +38,7 @@ export function NotificationFooter({ ? NotificationButtonRow({ folders, organizations, + i18n, primaryButton: { handlePrimaryButtonClick: handleSaveAction, text: primaryButtonText, diff --git a/apps/browser/src/autofill/notification/bar.ts b/apps/browser/src/autofill/notification/bar.ts index d660790ee63..4e85d893178 100644 --- a/apps/browser/src/autofill/notification/bar.ts +++ b/apps/browser/src/autofill/notification/bar.ts @@ -53,6 +53,7 @@ function getI18n() { return { appName: chrome.i18n.getMessage("appName"), close: chrome.i18n.getMessage("close"), + collection: chrome.i18n.getMessage("collection"), folder: chrome.i18n.getMessage("folder"), loginSaveSuccess: chrome.i18n.getMessage("loginSaveSuccess"), loginSaveSuccessDetails: chrome.i18n.getMessage("loginSaveSuccessDetails"), @@ -63,10 +64,11 @@ function getI18n() { nextSecurityTaskAction: chrome.i18n.getMessage("nextSecurityTaskAction"), newItem: chrome.i18n.getMessage("newItem"), never: chrome.i18n.getMessage("never"), + myVault: chrome.i18n.getMessage("myVault"), notificationAddDesc: chrome.i18n.getMessage("notificationAddDesc"), notificationAddSave: chrome.i18n.getMessage("notificationAddSave"), notificationChangeDesc: chrome.i18n.getMessage("notificationChangeDesc"), - notificationChangeSave: chrome.i18n.getMessage("notificationChangeSave"), + notificationUpdate: chrome.i18n.getMessage("notificationChangeSave"), notificationEdit: chrome.i18n.getMessage("edit"), notificationUnlock: chrome.i18n.getMessage("notificationUnlock"), notificationUnlockDesc: chrome.i18n.getMessage("notificationUnlockDesc"), @@ -78,6 +80,7 @@ function getI18n() { typeLogin: chrome.i18n.getMessage("typeLogin"), updateLoginAction: chrome.i18n.getMessage("updateLoginAction"), updateLoginPrompt: chrome.i18n.getMessage("updateLoginPrompt"), + vault: chrome.i18n.getMessage("vault"), view: chrome.i18n.getMessage("view"), }; } @@ -200,7 +203,7 @@ async function initNotificationBar(message: NotificationBarWindowMessage) { const changeTemplate = document.getElementById("template-change") as HTMLTemplateElement; const changeButton = findElementById(changeTemplate, "change-save"); - changeButton.textContent = i18n.notificationChangeSave; + changeButton.textContent = i18n.notificationUpdate; const changeEditButton = findElementById(changeTemplate, "change-edit"); changeEditButton.textContent = i18n.notificationEdit; From 3aa1378c992df4da6e035ca4a89931c1cf39cb87 Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Wed, 23 Apr 2025 16:59:14 +0200 Subject: [PATCH 039/499] Eliminate dead code from trial-initiation (#14378) --- .../abm-enterprise-content.component.html | 17 --- .../abm-enterprise-content.component.ts | 7 - .../content/abm-teams-content.component.html | 17 --- .../content/abm-teams-content.component.ts | 7 - .../cnet-enterprise-content.component.html | 17 --- .../cnet-enterprise-content.component.ts | 7 - .../cnet-individual-content.component.html | 17 --- .../cnet-individual-content.component.ts | 7 - .../content/cnet-teams-content.component.html | 17 --- .../content/cnet-teams-content.component.ts | 7 - .../content/default-content.component.html | 16 -- .../content/default-content.component.ts | 7 - .../content/enterprise-content.component.html | 44 ------ .../content/enterprise-content.component.ts | 7 - .../enterprise1-content.component.html | 44 ------ .../content/enterprise1-content.component.ts | 7 - .../enterprise2-content.component.html | 44 ------ .../content/enterprise2-content.component.ts | 7 - .../content/logo-badges.component.html | 11 -- .../content/logo-badges.component.ts | 7 - .../content/logo-cnet-5-stars.component.html | 23 --- .../content/logo-cnet-5-stars.component.ts | 7 - .../content/logo-cnet.component.html | 15 -- .../content/logo-cnet.component.ts | 7 - .../logo-company-testimonial.component.html | 28 ---- .../logo-company-testimonial.component.ts | 7 - .../content/logo-forbes.component.html | 15 -- .../content/logo-forbes.component.ts | 7 - .../content/logo-us-news.component.html | 5 - .../content/logo-us-news.component.ts | 7 - .../content/review-blurb.component.html | 13 -- .../content/review-blurb.component.ts | 13 -- .../content/review-logo.component.html | 18 --- .../content/review-logo.component.ts | 13 -- .../secrets-manager-content.component.html | 30 ---- .../secrets-manager-content.component.ts | 80 ---------- .../content/teams-content.component.html | 17 --- .../content/teams-content.component.ts | 7 - .../content/teams1-content.component.html | 35 ----- .../content/teams1-content.component.ts | 7 - .../content/teams2-content.component.html | 35 ----- .../content/teams2-content.component.ts | 7 - .../content/teams3-content.component.html | 26 ---- .../content/teams3-content.component.ts | 7 - ...-manager-trial-free-stepper.component.html | 45 ------ ...ts-manager-trial-free-stepper.component.ts | 90 ----------- ...-manager-trial-paid-stepper.component.html | 67 -------- ...ts-manager-trial-paid-stepper.component.ts | 144 ------------------ .../secrets-manager-trial.component.html | 44 ------ .../secrets-manager-trial.component.ts | 32 ---- .../trial-initiation.module.ts | 59 +------ 51 files changed, 1 insertion(+), 1223 deletions(-) delete mode 100644 apps/web/src/app/billing/trial-initiation/content/abm-enterprise-content.component.html delete mode 100644 apps/web/src/app/billing/trial-initiation/content/abm-enterprise-content.component.ts delete mode 100644 apps/web/src/app/billing/trial-initiation/content/abm-teams-content.component.html delete mode 100644 apps/web/src/app/billing/trial-initiation/content/abm-teams-content.component.ts delete mode 100644 apps/web/src/app/billing/trial-initiation/content/cnet-enterprise-content.component.html delete mode 100644 apps/web/src/app/billing/trial-initiation/content/cnet-enterprise-content.component.ts delete mode 100644 apps/web/src/app/billing/trial-initiation/content/cnet-individual-content.component.html delete mode 100644 apps/web/src/app/billing/trial-initiation/content/cnet-individual-content.component.ts delete mode 100644 apps/web/src/app/billing/trial-initiation/content/cnet-teams-content.component.html delete mode 100644 apps/web/src/app/billing/trial-initiation/content/cnet-teams-content.component.ts delete mode 100644 apps/web/src/app/billing/trial-initiation/content/default-content.component.html delete mode 100644 apps/web/src/app/billing/trial-initiation/content/default-content.component.ts delete mode 100644 apps/web/src/app/billing/trial-initiation/content/enterprise-content.component.html delete mode 100644 apps/web/src/app/billing/trial-initiation/content/enterprise-content.component.ts delete mode 100644 apps/web/src/app/billing/trial-initiation/content/enterprise1-content.component.html delete mode 100644 apps/web/src/app/billing/trial-initiation/content/enterprise1-content.component.ts delete mode 100644 apps/web/src/app/billing/trial-initiation/content/enterprise2-content.component.html delete mode 100644 apps/web/src/app/billing/trial-initiation/content/enterprise2-content.component.ts delete mode 100644 apps/web/src/app/billing/trial-initiation/content/logo-badges.component.html delete mode 100644 apps/web/src/app/billing/trial-initiation/content/logo-badges.component.ts delete mode 100644 apps/web/src/app/billing/trial-initiation/content/logo-cnet-5-stars.component.html delete mode 100644 apps/web/src/app/billing/trial-initiation/content/logo-cnet-5-stars.component.ts delete mode 100644 apps/web/src/app/billing/trial-initiation/content/logo-cnet.component.html delete mode 100644 apps/web/src/app/billing/trial-initiation/content/logo-cnet.component.ts delete mode 100644 apps/web/src/app/billing/trial-initiation/content/logo-company-testimonial.component.html delete mode 100644 apps/web/src/app/billing/trial-initiation/content/logo-company-testimonial.component.ts delete mode 100644 apps/web/src/app/billing/trial-initiation/content/logo-forbes.component.html delete mode 100644 apps/web/src/app/billing/trial-initiation/content/logo-forbes.component.ts delete mode 100644 apps/web/src/app/billing/trial-initiation/content/logo-us-news.component.html delete mode 100644 apps/web/src/app/billing/trial-initiation/content/logo-us-news.component.ts delete mode 100644 apps/web/src/app/billing/trial-initiation/content/review-blurb.component.html delete mode 100644 apps/web/src/app/billing/trial-initiation/content/review-blurb.component.ts delete mode 100644 apps/web/src/app/billing/trial-initiation/content/review-logo.component.html delete mode 100644 apps/web/src/app/billing/trial-initiation/content/review-logo.component.ts delete mode 100644 apps/web/src/app/billing/trial-initiation/content/secrets-manager-content.component.html delete mode 100644 apps/web/src/app/billing/trial-initiation/content/secrets-manager-content.component.ts delete mode 100644 apps/web/src/app/billing/trial-initiation/content/teams-content.component.html delete mode 100644 apps/web/src/app/billing/trial-initiation/content/teams-content.component.ts delete mode 100644 apps/web/src/app/billing/trial-initiation/content/teams1-content.component.html delete mode 100644 apps/web/src/app/billing/trial-initiation/content/teams1-content.component.ts delete mode 100644 apps/web/src/app/billing/trial-initiation/content/teams2-content.component.html delete mode 100644 apps/web/src/app/billing/trial-initiation/content/teams2-content.component.ts delete mode 100644 apps/web/src/app/billing/trial-initiation/content/teams3-content.component.html delete mode 100644 apps/web/src/app/billing/trial-initiation/content/teams3-content.component.ts delete mode 100644 apps/web/src/app/billing/trial-initiation/secrets-manager/secrets-manager-trial-free-stepper.component.html delete mode 100644 apps/web/src/app/billing/trial-initiation/secrets-manager/secrets-manager-trial-free-stepper.component.ts delete mode 100644 apps/web/src/app/billing/trial-initiation/secrets-manager/secrets-manager-trial-paid-stepper.component.html delete mode 100644 apps/web/src/app/billing/trial-initiation/secrets-manager/secrets-manager-trial-paid-stepper.component.ts delete mode 100644 apps/web/src/app/billing/trial-initiation/secrets-manager/secrets-manager-trial.component.html delete mode 100644 apps/web/src/app/billing/trial-initiation/secrets-manager/secrets-manager-trial.component.ts diff --git a/apps/web/src/app/billing/trial-initiation/content/abm-enterprise-content.component.html b/apps/web/src/app/billing/trial-initiation/content/abm-enterprise-content.component.html deleted file mode 100644 index 46e1fae80df..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/abm-enterprise-content.component.html +++ /dev/null @@ -1,17 +0,0 @@ -

The Bitwarden Password Manager

-
-

- Trusted by millions of individuals, teams, and organizations worldwide for secure password - storage and sharing. -

-
-
    -
  • Store logins, secure notes, and more
  • -
  • Collaborate and share securely
  • -
  • Access anywhere on any device
  • -
  • Create your account to get started
  • -
-
- - -
diff --git a/apps/web/src/app/billing/trial-initiation/content/abm-enterprise-content.component.ts b/apps/web/src/app/billing/trial-initiation/content/abm-enterprise-content.component.ts deleted file mode 100644 index 0f9db7b4405..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/abm-enterprise-content.component.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Component } from "@angular/core"; - -@Component({ - selector: "app-abm-enterprise-content", - templateUrl: "abm-enterprise-content.component.html", -}) -export class AbmEnterpriseContentComponent {} diff --git a/apps/web/src/app/billing/trial-initiation/content/abm-teams-content.component.html b/apps/web/src/app/billing/trial-initiation/content/abm-teams-content.component.html deleted file mode 100644 index 46e1fae80df..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/abm-teams-content.component.html +++ /dev/null @@ -1,17 +0,0 @@ -

The Bitwarden Password Manager

-
-

- Trusted by millions of individuals, teams, and organizations worldwide for secure password - storage and sharing. -

-
-
    -
  • Store logins, secure notes, and more
  • -
  • Collaborate and share securely
  • -
  • Access anywhere on any device
  • -
  • Create your account to get started
  • -
-
- - -
diff --git a/apps/web/src/app/billing/trial-initiation/content/abm-teams-content.component.ts b/apps/web/src/app/billing/trial-initiation/content/abm-teams-content.component.ts deleted file mode 100644 index 7765555f5cc..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/abm-teams-content.component.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Component } from "@angular/core"; - -@Component({ - selector: "app-abm-teams-content", - templateUrl: "abm-teams-content.component.html", -}) -export class AbmTeamsContentComponent {} diff --git a/apps/web/src/app/billing/trial-initiation/content/cnet-enterprise-content.component.html b/apps/web/src/app/billing/trial-initiation/content/cnet-enterprise-content.component.html deleted file mode 100644 index b5c16911ab0..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/cnet-enterprise-content.component.html +++ /dev/null @@ -1,17 +0,0 @@ -

Start Your Enterprise Free Trial Now

-
-

- Millions of individuals, teams, and organizations worldwide trust Bitwarden for secure password - storage and sharing. -

-
-
    -
  • Collaborate and share securely
  • -
  • Deploy and manage quickly and easily
  • -
  • Access anywhere on any device
  • -
  • Create your account to get started
  • -
-
- - -
diff --git a/apps/web/src/app/billing/trial-initiation/content/cnet-enterprise-content.component.ts b/apps/web/src/app/billing/trial-initiation/content/cnet-enterprise-content.component.ts deleted file mode 100644 index 4a6de8d3003..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/cnet-enterprise-content.component.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Component } from "@angular/core"; - -@Component({ - selector: "app-cnet-enterprise-content", - templateUrl: "cnet-enterprise-content.component.html", -}) -export class CnetEnterpriseContentComponent {} diff --git a/apps/web/src/app/billing/trial-initiation/content/cnet-individual-content.component.html b/apps/web/src/app/billing/trial-initiation/content/cnet-individual-content.component.html deleted file mode 100644 index 6e6f545c170..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/cnet-individual-content.component.html +++ /dev/null @@ -1,17 +0,0 @@ -

Start Your Premium Account Now

-
-

- Millions of individuals, teams, and organizations worldwide trust Bitwarden for secure password - storage and sharing. -

-
-
    -
  • Store logins, secure notes, and more
  • -
  • Secure your account with advanced two-step login
  • -
  • Access anywhere on any device
  • -
  • Create your account to get started
  • -
-
- - -
diff --git a/apps/web/src/app/billing/trial-initiation/content/cnet-individual-content.component.ts b/apps/web/src/app/billing/trial-initiation/content/cnet-individual-content.component.ts deleted file mode 100644 index 56d8b37af90..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/cnet-individual-content.component.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Component } from "@angular/core"; - -@Component({ - selector: "app-cnet-individual-content", - templateUrl: "cnet-individual-content.component.html", -}) -export class CnetIndividualContentComponent {} diff --git a/apps/web/src/app/billing/trial-initiation/content/cnet-teams-content.component.html b/apps/web/src/app/billing/trial-initiation/content/cnet-teams-content.component.html deleted file mode 100644 index c719c5ac7ce..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/cnet-teams-content.component.html +++ /dev/null @@ -1,17 +0,0 @@ -

Start Your Teams Free Trial Now

-
-

- Millions of individuals, teams, and organizations worldwide trust Bitwarden for secure password - storage and sharing. -

-
-
    -
  • Collaborate and share securely
  • -
  • Deploy and manage quickly and easily
  • -
  • Access anywhere on any device
  • -
  • Create your account to get started
  • -
-
- - -
diff --git a/apps/web/src/app/billing/trial-initiation/content/cnet-teams-content.component.ts b/apps/web/src/app/billing/trial-initiation/content/cnet-teams-content.component.ts deleted file mode 100644 index ff79a0d37cd..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/cnet-teams-content.component.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Component } from "@angular/core"; - -@Component({ - selector: "app-cnet-teams-content", - templateUrl: "cnet-teams-content.component.html", -}) -export class CnetTeamsContentComponent {} diff --git a/apps/web/src/app/billing/trial-initiation/content/default-content.component.html b/apps/web/src/app/billing/trial-initiation/content/default-content.component.html deleted file mode 100644 index e1839517ff6..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/default-content.component.html +++ /dev/null @@ -1,16 +0,0 @@ -

The Bitwarden Password Manager

-
-

- Trusted by millions of individuals, teams, and organizations worldwide for secure password - storage and sharing. -

-
-
    -
  • Store logins, secure notes, and more
  • -
  • Collaborate and share securely
  • -
  • Access anywhere on any device
  • -
  • Create your account to get started
  • -
-
- -
diff --git a/apps/web/src/app/billing/trial-initiation/content/default-content.component.ts b/apps/web/src/app/billing/trial-initiation/content/default-content.component.ts deleted file mode 100644 index 7ad40b089d1..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/default-content.component.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Component } from "@angular/core"; - -@Component({ - selector: "app-default-content", - templateUrl: "default-content.component.html", -}) -export class DefaultContentComponent {} diff --git a/apps/web/src/app/billing/trial-initiation/content/enterprise-content.component.html b/apps/web/src/app/billing/trial-initiation/content/enterprise-content.component.html deleted file mode 100644 index f57fb7a3510..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/enterprise-content.component.html +++ /dev/null @@ -1,44 +0,0 @@ -

Start your 7-day Enterprise free trial

-
-

- Bitwarden is the most trusted password manager designed for seamless administration and employee - usability. -

-
-
    -
  • - Instantly and securely share credentials with the groups and individuals who need them -
  • -
  • - Strengthen company-wide security through centralized administrative control and - policies -
  • -
  • - Streamline user onboarding and automate account provisioning with flexible SSO and SCIM - integrations -
  • -
  • - Migrate to Bitwarden in minutes with comprehensive import options -
  • -
  • - Give all Enterprise users the gift of 360º security with a free Families plan -
  • -
-
- -
diff --git a/apps/web/src/app/billing/trial-initiation/content/enterprise-content.component.ts b/apps/web/src/app/billing/trial-initiation/content/enterprise-content.component.ts deleted file mode 100644 index 847b3c3088a..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/enterprise-content.component.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Component } from "@angular/core"; - -@Component({ - selector: "app-enterprise-content", - templateUrl: "enterprise-content.component.html", -}) -export class EnterpriseContentComponent {} diff --git a/apps/web/src/app/billing/trial-initiation/content/enterprise1-content.component.html b/apps/web/src/app/billing/trial-initiation/content/enterprise1-content.component.html deleted file mode 100644 index f57fb7a3510..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/enterprise1-content.component.html +++ /dev/null @@ -1,44 +0,0 @@ -

Start your 7-day Enterprise free trial

-
-

- Bitwarden is the most trusted password manager designed for seamless administration and employee - usability. -

-
-
    -
  • - Instantly and securely share credentials with the groups and individuals who need them -
  • -
  • - Strengthen company-wide security through centralized administrative control and - policies -
  • -
  • - Streamline user onboarding and automate account provisioning with flexible SSO and SCIM - integrations -
  • -
  • - Migrate to Bitwarden in minutes with comprehensive import options -
  • -
  • - Give all Enterprise users the gift of 360º security with a free Families plan -
  • -
-
- -
diff --git a/apps/web/src/app/billing/trial-initiation/content/enterprise1-content.component.ts b/apps/web/src/app/billing/trial-initiation/content/enterprise1-content.component.ts deleted file mode 100644 index 7b1199eb421..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/enterprise1-content.component.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Component } from "@angular/core"; - -@Component({ - selector: "app-enterprise1-content", - templateUrl: "enterprise1-content.component.html", -}) -export class Enterprise1ContentComponent {} diff --git a/apps/web/src/app/billing/trial-initiation/content/enterprise2-content.component.html b/apps/web/src/app/billing/trial-initiation/content/enterprise2-content.component.html deleted file mode 100644 index f57fb7a3510..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/enterprise2-content.component.html +++ /dev/null @@ -1,44 +0,0 @@ -

Start your 7-day Enterprise free trial

-
-

- Bitwarden is the most trusted password manager designed for seamless administration and employee - usability. -

-
-
    -
  • - Instantly and securely share credentials with the groups and individuals who need them -
  • -
  • - Strengthen company-wide security through centralized administrative control and - policies -
  • -
  • - Streamline user onboarding and automate account provisioning with flexible SSO and SCIM - integrations -
  • -
  • - Migrate to Bitwarden in minutes with comprehensive import options -
  • -
  • - Give all Enterprise users the gift of 360º security with a free Families plan -
  • -
-
- -
diff --git a/apps/web/src/app/billing/trial-initiation/content/enterprise2-content.component.ts b/apps/web/src/app/billing/trial-initiation/content/enterprise2-content.component.ts deleted file mode 100644 index 08dec6190c7..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/enterprise2-content.component.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Component } from "@angular/core"; - -@Component({ - selector: "app-enterprise2-content", - templateUrl: "enterprise2-content.component.html", -}) -export class Enterprise2ContentComponent {} diff --git a/apps/web/src/app/billing/trial-initiation/content/logo-badges.component.html b/apps/web/src/app/billing/trial-initiation/content/logo-badges.component.html deleted file mode 100644 index d1b33eab3a4..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/logo-badges.component.html +++ /dev/null @@ -1,11 +0,0 @@ -
-
- - third party awards - -
-
diff --git a/apps/web/src/app/billing/trial-initiation/content/logo-badges.component.ts b/apps/web/src/app/billing/trial-initiation/content/logo-badges.component.ts deleted file mode 100644 index c23432b67cf..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/logo-badges.component.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Component } from "@angular/core"; - -@Component({ - selector: "app-logo-badges", - templateUrl: "logo-badges.component.html", -}) -export class LogoBadgesComponent {} diff --git a/apps/web/src/app/billing/trial-initiation/content/logo-cnet-5-stars.component.html b/apps/web/src/app/billing/trial-initiation/content/logo-cnet-5-stars.component.html deleted file mode 100644 index fb4537d2820..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/logo-cnet-5-stars.component.html +++ /dev/null @@ -1,23 +0,0 @@ -
-
- - - - - -
-
- “Bitwarden scores points for being fully open-source, secure and audited annually by third-party - cybersecurity firms, giving it a level of transparency that sets it apart from its peers.” -
-
- - CNET Logo - -

Best Password Manager in 2024

-
-
diff --git a/apps/web/src/app/billing/trial-initiation/content/logo-cnet-5-stars.component.ts b/apps/web/src/app/billing/trial-initiation/content/logo-cnet-5-stars.component.ts deleted file mode 100644 index af531829d50..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/logo-cnet-5-stars.component.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Component } from "@angular/core"; - -@Component({ - selector: "app-logo-cnet-5-stars", - templateUrl: "logo-cnet-5-stars.component.html", -}) -export class LogoCnet5StarsComponent {} diff --git a/apps/web/src/app/billing/trial-initiation/content/logo-cnet.component.html b/apps/web/src/app/billing/trial-initiation/content/logo-cnet.component.html deleted file mode 100644 index 4e04cec6da4..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/logo-cnet.component.html +++ /dev/null @@ -1,15 +0,0 @@ -
-
- - CNET Logo - -
-
- "No more excuses; start using Bitwarden today. The identity you save could be your own. The - money definitely will be." -
-
diff --git a/apps/web/src/app/billing/trial-initiation/content/logo-cnet.component.ts b/apps/web/src/app/billing/trial-initiation/content/logo-cnet.component.ts deleted file mode 100644 index 4f755f66a86..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/logo-cnet.component.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Component } from "@angular/core"; - -@Component({ - selector: "app-logo-cnet", - templateUrl: "logo-cnet.component.html", -}) -export class LogoCnetComponent {} diff --git a/apps/web/src/app/billing/trial-initiation/content/logo-company-testimonial.component.html b/apps/web/src/app/billing/trial-initiation/content/logo-company-testimonial.component.html deleted file mode 100644 index 0b81e0bd216..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/logo-company-testimonial.component.html +++ /dev/null @@ -1,28 +0,0 @@ -
-

- Recommended by industry experts -

-
-
- CNET Logo - WIRED Logo -
-
- New York Times Logo - PC Mag Logo -
-
-
- “Bitwarden is currently CNET's top pick for the best password manager, thanks in part to - its commitment to transparency and its unbeatable free tier.” -
-

Best Password Manager in 2024

-
diff --git a/apps/web/src/app/billing/trial-initiation/content/logo-company-testimonial.component.ts b/apps/web/src/app/billing/trial-initiation/content/logo-company-testimonial.component.ts deleted file mode 100644 index 9d9c4471820..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/logo-company-testimonial.component.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Component } from "@angular/core"; - -@Component({ - selector: "app-logo-company-testimonial", - templateUrl: "logo-company-testimonial.component.html", -}) -export class LogoCompanyTestimonialComponent {} diff --git a/apps/web/src/app/billing/trial-initiation/content/logo-forbes.component.html b/apps/web/src/app/billing/trial-initiation/content/logo-forbes.component.html deleted file mode 100644 index 34426168324..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/logo-forbes.component.html +++ /dev/null @@ -1,15 +0,0 @@ -
-
- - Forbes Logo - -
-
- “Bitwarden boasts the backing of some of the world's best security experts and an attractive, - easy-to-use interface” -
-
diff --git a/apps/web/src/app/billing/trial-initiation/content/logo-forbes.component.ts b/apps/web/src/app/billing/trial-initiation/content/logo-forbes.component.ts deleted file mode 100644 index 818721fd1e9..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/logo-forbes.component.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Component } from "@angular/core"; - -@Component({ - selector: "app-logo-forbes", - templateUrl: "logo-forbes.component.html", -}) -export class LogoForbesComponent {} diff --git a/apps/web/src/app/billing/trial-initiation/content/logo-us-news.component.html b/apps/web/src/app/billing/trial-initiation/content/logo-us-news.component.html deleted file mode 100644 index bd44b56f090..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/logo-us-news.component.html +++ /dev/null @@ -1,5 +0,0 @@ -US News 360 Reviews Best Password Manager diff --git a/apps/web/src/app/billing/trial-initiation/content/logo-us-news.component.ts b/apps/web/src/app/billing/trial-initiation/content/logo-us-news.component.ts deleted file mode 100644 index fb0b1e0c71b..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/logo-us-news.component.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Component } from "@angular/core"; - -@Component({ - selector: "app-logo-us-news", - templateUrl: "logo-us-news.component.html", -}) -export class LogoUSNewsComponent {} diff --git a/apps/web/src/app/billing/trial-initiation/content/review-blurb.component.html b/apps/web/src/app/billing/trial-initiation/content/review-blurb.component.html deleted file mode 100644 index cd719a35af8..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/review-blurb.component.html +++ /dev/null @@ -1,13 +0,0 @@ -
-

- {{ header }} -

-
- "{{ quote }}" -
-
- -

{{ source }}

-
-
-
diff --git a/apps/web/src/app/billing/trial-initiation/content/review-blurb.component.ts b/apps/web/src/app/billing/trial-initiation/content/review-blurb.component.ts deleted file mode 100644 index 6419ddf1e45..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/review-blurb.component.ts +++ /dev/null @@ -1,13 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Component, Input } from "@angular/core"; - -@Component({ - selector: "app-review-blurb", - templateUrl: "review-blurb.component.html", -}) -export class ReviewBlurbComponent { - @Input() header: string; - @Input() quote: string; - @Input() source: string; -} diff --git a/apps/web/src/app/billing/trial-initiation/content/review-logo.component.html b/apps/web/src/app/billing/trial-initiation/content/review-logo.component.html deleted file mode 100644 index 77f592f1c45..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/review-logo.component.html +++ /dev/null @@ -1,18 +0,0 @@ -
- -
-
- - - - -
-
- -
- -
-
- 4.7 -
-
diff --git a/apps/web/src/app/billing/trial-initiation/content/review-logo.component.ts b/apps/web/src/app/billing/trial-initiation/content/review-logo.component.ts deleted file mode 100644 index 9b104ac0bc3..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/review-logo.component.ts +++ /dev/null @@ -1,13 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Component, Input } from "@angular/core"; - -@Component({ - selector: "review-logo", - templateUrl: "review-logo.component.html", -}) -export class ReviewLogoComponent { - @Input() logoClass: string; - @Input() logoSrc: string; - @Input() logoAlt: string; -} diff --git a/apps/web/src/app/billing/trial-initiation/content/secrets-manager-content.component.html b/apps/web/src/app/billing/trial-initiation/content/secrets-manager-content.component.html deleted file mode 100644 index 569ff91f625..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/secrets-manager-content.component.html +++ /dev/null @@ -1,30 +0,0 @@ -

{{ header }}

-
-

- {{ headline }} -

-
-
    -
  • - {{ primaryPoint }} -
  • -
-
-
-
-

{{ calloutHeadline }}

-
    -
  • - {{ callout }} -
  • -
-
-
-
-
- -
diff --git a/apps/web/src/app/billing/trial-initiation/content/secrets-manager-content.component.ts b/apps/web/src/app/billing/trial-initiation/content/secrets-manager-content.component.ts deleted file mode 100644 index 955c18fddf2..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/secrets-manager-content.component.ts +++ /dev/null @@ -1,80 +0,0 @@ -// 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 { ActivatedRoute } from "@angular/router"; -import { Subject, takeUntil } from "rxjs"; - -@Component({ - selector: "app-secrets-manager-content", - templateUrl: "secrets-manager-content.component.html", -}) -export class SecretsManagerContentComponent implements OnInit, OnDestroy { - header: string; - headline = - "A simpler, faster way to secure and automate secrets across code and infrastructure deployments"; - primaryPoints: string[]; - calloutHeadline: string; - callouts: string[]; - - private paidPrimaryPoints = [ - "Unlimited secrets, users, and projects", - "Simple and transparent pricing", - "Zero-knowledge, end-to-end encryption", - ]; - - private paidCalloutHeadline = "Limited time offer"; - - private paidCallouts = [ - "Sign up today and receive a complimentary 12-month subscription to Bitwarden Password Manager", - "Experience complete security across your organization", - "Secure all your sensitive credentials, from user applications to machine secrets", - ]; - - private freePrimaryPoints = [ - "Unlimited secrets", - "Simple and transparent pricing", - "Zero-knowledge, end-to-end encryption", - ]; - - private freeCalloutHeadline = "Go beyond developer security!"; - - private freeCallouts = [ - "Your Bitwarden account will also grant complimentary access to Bitwarden Password Manager", - "Extend end-to-end encryption to your personal passwords, addresses, credit cards and notes", - ]; - - private destroy$ = new Subject(); - - constructor(private activatedRoute: ActivatedRoute) {} - - ngOnDestroy() { - this.destroy$.next(); - this.destroy$.complete(); - } - - ngOnInit(): void { - this.activatedRoute.queryParams.pipe(takeUntil(this.destroy$)).subscribe((queryParameters) => { - switch (queryParameters.org) { - case "enterprise": - this.header = "Secrets Manager for Enterprise"; - this.primaryPoints = this.paidPrimaryPoints; - this.calloutHeadline = this.paidCalloutHeadline; - this.callouts = this.paidCallouts; - break; - case "free": - this.header = "Bitwarden Secrets Manager"; - this.primaryPoints = this.freePrimaryPoints; - this.calloutHeadline = this.freeCalloutHeadline; - this.callouts = this.freeCallouts; - break; - case "teams": - case "teamsStarter": - this.header = "Secrets Manager for Teams"; - this.primaryPoints = this.paidPrimaryPoints; - this.calloutHeadline = this.paidCalloutHeadline; - this.callouts = this.paidCallouts; - break; - } - }); - } -} diff --git a/apps/web/src/app/billing/trial-initiation/content/teams-content.component.html b/apps/web/src/app/billing/trial-initiation/content/teams-content.component.html deleted file mode 100644 index 46e1fae80df..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/teams-content.component.html +++ /dev/null @@ -1,17 +0,0 @@ -

The Bitwarden Password Manager

-
-

- Trusted by millions of individuals, teams, and organizations worldwide for secure password - storage and sharing. -

-
-
    -
  • Store logins, secure notes, and more
  • -
  • Collaborate and share securely
  • -
  • Access anywhere on any device
  • -
  • Create your account to get started
  • -
-
- - -
diff --git a/apps/web/src/app/billing/trial-initiation/content/teams-content.component.ts b/apps/web/src/app/billing/trial-initiation/content/teams-content.component.ts deleted file mode 100644 index 5c97695deff..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/teams-content.component.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Component } from "@angular/core"; - -@Component({ - selector: "app-teams-content", - templateUrl: "teams-content.component.html", -}) -export class TeamsContentComponent {} diff --git a/apps/web/src/app/billing/trial-initiation/content/teams1-content.component.html b/apps/web/src/app/billing/trial-initiation/content/teams1-content.component.html deleted file mode 100644 index f51c370bebd..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/teams1-content.component.html +++ /dev/null @@ -1,35 +0,0 @@ -

Start your 7-day free trial for Teams

-
-

- Strengthen business security with an easy-to-use password manager your team will love. -

-
-
    -
  • - Instantly and securely share credentials with the groups and individuals who need them -
  • -
  • - Migrate to Bitwarden in minutes with comprehensive import options -
  • -
  • - Save time and increase productivity with autofill and instant device syncing -
  • -
  • - Enhance security practices across your team with easy user management -
  • -
-
- -
diff --git a/apps/web/src/app/billing/trial-initiation/content/teams1-content.component.ts b/apps/web/src/app/billing/trial-initiation/content/teams1-content.component.ts deleted file mode 100644 index 055ec7fda10..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/teams1-content.component.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Component } from "@angular/core"; - -@Component({ - selector: "app-teams1-content", - templateUrl: "teams1-content.component.html", -}) -export class Teams1ContentComponent {} diff --git a/apps/web/src/app/billing/trial-initiation/content/teams2-content.component.html b/apps/web/src/app/billing/trial-initiation/content/teams2-content.component.html deleted file mode 100644 index f51c370bebd..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/teams2-content.component.html +++ /dev/null @@ -1,35 +0,0 @@ -

Start your 7-day free trial for Teams

-
-

- Strengthen business security with an easy-to-use password manager your team will love. -

-
-
    -
  • - Instantly and securely share credentials with the groups and individuals who need them -
  • -
  • - Migrate to Bitwarden in minutes with comprehensive import options -
  • -
  • - Save time and increase productivity with autofill and instant device syncing -
  • -
  • - Enhance security practices across your team with easy user management -
  • -
-
- -
diff --git a/apps/web/src/app/billing/trial-initiation/content/teams2-content.component.ts b/apps/web/src/app/billing/trial-initiation/content/teams2-content.component.ts deleted file mode 100644 index 394ba90b491..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/teams2-content.component.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Component } from "@angular/core"; - -@Component({ - selector: "app-teams2-content", - templateUrl: "teams2-content.component.html", -}) -export class Teams2ContentComponent {} diff --git a/apps/web/src/app/billing/trial-initiation/content/teams3-content.component.html b/apps/web/src/app/billing/trial-initiation/content/teams3-content.component.html deleted file mode 100644 index c6f1ae697ae..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/teams3-content.component.html +++ /dev/null @@ -1,26 +0,0 @@ -

Begin Teams Starter Free Trial Now

-
-

- Millions of individuals, teams, and organizations worldwide trust Bitwarden for secure password - storage and sharing. -

-
-
    -
  • - Powerful security for up to 10 users -
    - Have more than 10 users? - Start a Teams trial -
    -
  • -
  • Collaborate and share securely
  • -
  • Deploy and manage quickly and easily
  • -
  • Access anywhere on any device
  • -
  • Create your account to get started
  • -
-
- - -
diff --git a/apps/web/src/app/billing/trial-initiation/content/teams3-content.component.ts b/apps/web/src/app/billing/trial-initiation/content/teams3-content.component.ts deleted file mode 100644 index df91268ab26..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/teams3-content.component.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Component } from "@angular/core"; - -@Component({ - selector: "app-teams3-content", - templateUrl: "teams3-content.component.html", -}) -export class Teams3ContentComponent {} diff --git a/apps/web/src/app/billing/trial-initiation/secrets-manager/secrets-manager-trial-free-stepper.component.html b/apps/web/src/app/billing/trial-initiation/secrets-manager/secrets-manager-trial-free-stepper.component.html deleted file mode 100644 index dddac598a46..00000000000 --- a/apps/web/src/app/billing/trial-initiation/secrets-manager/secrets-manager-trial-free-stepper.component.html +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - -
-

{{ "smFreeTrialThankYou" | i18n }}

-
    -
  • -

    - {{ "smFreeTrialConfirmationEmail" | i18n }} - {{ formGroup.get("email").value }}. -

    -
  • -
-
-
- - -
-
-
diff --git a/apps/web/src/app/billing/trial-initiation/secrets-manager/secrets-manager-trial-free-stepper.component.ts b/apps/web/src/app/billing/trial-initiation/secrets-manager/secrets-manager-trial-free-stepper.component.ts deleted file mode 100644 index f7c5a9b2b98..00000000000 --- a/apps/web/src/app/billing/trial-initiation/secrets-manager/secrets-manager-trial-free-stepper.component.ts +++ /dev/null @@ -1,90 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Component, OnInit, ViewChild } from "@angular/core"; -import { UntypedFormBuilder, Validators } from "@angular/forms"; -import { Router } from "@angular/router"; - -import { OrganizationBillingServiceAbstraction as OrganizationBillingService } from "@bitwarden/common/billing/abstractions/organization-billing.service"; -import { PlanType } from "@bitwarden/common/billing/enums"; -import { ReferenceEventRequest } from "@bitwarden/common/models/request/reference-event.request"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; - -import { VerticalStepperComponent } from "../../trial-initiation/vertical-stepper/vertical-stepper.component"; - -@Component({ - selector: "app-secrets-manager-trial-free-stepper", - templateUrl: "secrets-manager-trial-free-stepper.component.html", -}) -export class SecretsManagerTrialFreeStepperComponent implements OnInit { - @ViewChild("stepper", { static: false }) verticalStepper: VerticalStepperComponent; - - formGroup = this.formBuilder.group({ - name: [ - "", - { - validators: [Validators.required, Validators.maxLength(50)], - updateOn: "change", - }, - ], - email: [ - "", - { - validators: [Validators.email], - }, - ], - }); - - subLabels = { - createAccount: - "Before creating your free organization, you first need to log in or create a personal account.", - organizationInfo: "Enter your organization information", - }; - - organizationId: string; - - referenceEventRequest: ReferenceEventRequest; - - constructor( - protected formBuilder: UntypedFormBuilder, - protected i18nService: I18nService, - protected organizationBillingService: OrganizationBillingService, - protected router: Router, - ) {} - - ngOnInit(): void { - this.referenceEventRequest = new ReferenceEventRequest(); - this.referenceEventRequest.initiationPath = "Secrets Manager trial from marketing website"; - } - - accountCreated(email: string): void { - this.formGroup.get("email")?.setValue(email); - this.subLabels.createAccount = email; - this.verticalStepper.next(); - } - - async createOrganization(): Promise { - const response = await this.organizationBillingService.startFree({ - organization: { - name: this.formGroup.get("name").value, - billingEmail: this.formGroup.get("email").value, - }, - plan: { - type: PlanType.Free, - subscribeToSecretsManager: true, - isFromSecretsManagerTrial: true, - }, - }); - - this.organizationId = response.id; - this.subLabels.organizationInfo = response.name; - this.verticalStepper.next(); - } - - async navigateToMembers(): Promise { - await this.router.navigate(["organizations", this.organizationId, "members"]); - } - - async navigateToSecretsManager(): Promise { - await this.router.navigate(["sm", this.organizationId]); - } -} diff --git a/apps/web/src/app/billing/trial-initiation/secrets-manager/secrets-manager-trial-paid-stepper.component.html b/apps/web/src/app/billing/trial-initiation/secrets-manager/secrets-manager-trial-paid-stepper.component.html deleted file mode 100644 index 99e2706d713..00000000000 --- a/apps/web/src/app/billing/trial-initiation/secrets-manager/secrets-manager-trial-paid-stepper.component.html +++ /dev/null @@ -1,67 +0,0 @@ - - - - - - - - - - - -
- - -
-
-
diff --git a/apps/web/src/app/billing/trial-initiation/secrets-manager/secrets-manager-trial-paid-stepper.component.ts b/apps/web/src/app/billing/trial-initiation/secrets-manager/secrets-manager-trial-paid-stepper.component.ts deleted file mode 100644 index 650c1d8e69e..00000000000 --- a/apps/web/src/app/billing/trial-initiation/secrets-manager/secrets-manager-trial-paid-stepper.component.ts +++ /dev/null @@ -1,144 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Component, Input, OnInit, ViewChild } from "@angular/core"; -import { UntypedFormBuilder } from "@angular/forms"; -import { ActivatedRoute, Router } from "@angular/router"; -import { Subject, takeUntil } from "rxjs"; - -import { OrganizationBillingServiceAbstraction as OrganizationBillingService } from "@bitwarden/common/billing/abstractions/organization-billing.service"; -import { PlanType, ProductTierType } from "@bitwarden/common/billing/enums"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ReferenceEventRequest } from "@bitwarden/common/models/request/reference-event.request"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; - -import { - OrganizationCreatedEvent, - SubscriptionProduct, - TrialOrganizationType, -} from "../../../billing/accounts/trial-initiation/trial-billing-step.component"; -import { VerticalStepperComponent } from "../../trial-initiation/vertical-stepper/vertical-stepper.component"; -import { SecretsManagerTrialFreeStepperComponent } from "../secrets-manager/secrets-manager-trial-free-stepper.component"; - -export enum ValidOrgParams { - families = "families", - enterprise = "enterprise", - teams = "teams", - teamsStarter = "teamsStarter", - individual = "individual", - premium = "premium", - free = "free", -} - -const trialFlowOrgs = [ - ValidOrgParams.teams, - ValidOrgParams.teamsStarter, - ValidOrgParams.enterprise, - ValidOrgParams.families, -]; - -@Component({ - selector: "app-secrets-manager-trial-paid-stepper", - templateUrl: "secrets-manager-trial-paid-stepper.component.html", -}) -export class SecretsManagerTrialPaidStepperComponent - extends SecretsManagerTrialFreeStepperComponent - implements OnInit -{ - @ViewChild("stepper", { static: false }) verticalStepper: VerticalStepperComponent; - @Input() organizationTypeQueryParameter: string; - - plan: PlanType; - createOrganizationLoading = false; - billingSubLabel = this.i18nService.t("billingTrialSubLabel"); - organizationId: string; - - private destroy$ = new Subject(); - protected enableTrialPayment$ = this.configService.getFeatureFlag$( - FeatureFlag.TrialPaymentOptional, - ); - - constructor( - private route: ActivatedRoute, - private configService: ConfigService, - protected formBuilder: UntypedFormBuilder, - protected i18nService: I18nService, - protected organizationBillingService: OrganizationBillingService, - protected router: Router, - ) { - super(formBuilder, i18nService, organizationBillingService, router); - } - - async ngOnInit(): Promise { - this.referenceEventRequest = new ReferenceEventRequest(); - this.referenceEventRequest.initiationPath = "Secrets Manager trial from marketing website"; - - this.route.queryParams.pipe(takeUntil(this.destroy$)).subscribe((qParams) => { - if (trialFlowOrgs.includes(qParams.org)) { - if (qParams.org === ValidOrgParams.teamsStarter) { - this.plan = PlanType.TeamsStarter; - } else if (qParams.org === ValidOrgParams.teams) { - this.plan = PlanType.TeamsAnnually; - } else if (qParams.org === ValidOrgParams.enterprise) { - this.plan = PlanType.EnterpriseAnnually; - } - } - }); - } - - organizationCreated(event: OrganizationCreatedEvent) { - this.organizationId = event.organizationId; - this.billingSubLabel = event.planDescription; - this.verticalStepper.next(); - } - - steppedBack() { - this.verticalStepper.previous(); - } - - async createOrganizationOnTrial(): Promise { - this.createOrganizationLoading = true; - const response = await this.organizationBillingService.purchaseSubscriptionNoPaymentMethod({ - organization: { - name: this.formGroup.get("name").value, - billingEmail: this.formGroup.get("email").value, - initiationPath: "Secrets Manager trial from marketing website", - }, - plan: { - type: this.plan, - subscribeToSecretsManager: true, - isFromSecretsManagerTrial: true, - passwordManagerSeats: 1, - secretsManagerSeats: 1, - }, - }); - - this.organizationId = response?.id; - this.subLabels.organizationInfo = response?.name; - this.createOrganizationLoading = false; - this.verticalStepper.next(); - } - - get createAccountLabel() { - const organizationType = - this.productType === ProductTierType.TeamsStarter - ? "Teams Starter" - : ProductTierType[this.productType]; - return `Before creating your ${organizationType} organization, you first need to log in or create a personal account.`; - } - - get productType(): TrialOrganizationType { - switch (this.organizationTypeQueryParameter) { - case "enterprise": - return ProductTierType.Enterprise; - case "families": - return ProductTierType.Families; - case "teams": - return ProductTierType.Teams; - case "teamsStarter": - return ProductTierType.TeamsStarter; - } - } - - protected readonly SubscriptionProduct = SubscriptionProduct; -} diff --git a/apps/web/src/app/billing/trial-initiation/secrets-manager/secrets-manager-trial.component.html b/apps/web/src/app/billing/trial-initiation/secrets-manager/secrets-manager-trial.component.html deleted file mode 100644 index 88251136dbe..00000000000 --- a/apps/web/src/app/billing/trial-initiation/secrets-manager/secrets-manager-trial.component.html +++ /dev/null @@ -1,44 +0,0 @@ - - -
-
-
- Bitwarden -
- -
-
-
-
-
-
-

- {{ - "startYour7DayFreeTrialOfBitwardenSecretsManagerFor" - | i18n: organizationTypeQueryParameter - }} -

- -
- - -
-
-
-
-
diff --git a/apps/web/src/app/billing/trial-initiation/secrets-manager/secrets-manager-trial.component.ts b/apps/web/src/app/billing/trial-initiation/secrets-manager/secrets-manager-trial.component.ts deleted file mode 100644 index 678514532ca..00000000000 --- a/apps/web/src/app/billing/trial-initiation/secrets-manager/secrets-manager-trial.component.ts +++ /dev/null @@ -1,32 +0,0 @@ -// 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 { ActivatedRoute } from "@angular/router"; -import { Subject, takeUntil } from "rxjs"; - -@Component({ - selector: "app-secrets-manager-trial", - templateUrl: "secrets-manager-trial.component.html", -}) -export class SecretsManagerTrialComponent implements OnInit, OnDestroy { - organizationTypeQueryParameter: string; - - private destroy$ = new Subject(); - - constructor(private route: ActivatedRoute) {} - - ngOnInit(): void { - this.route.queryParams.pipe(takeUntil(this.destroy$)).subscribe((queryParameters) => { - this.organizationTypeQueryParameter = queryParameters.org; - }); - } - - ngOnDestroy(): void { - this.destroy$.next(); - this.destroy$.complete(); - } - - get freeOrganization() { - return this.organizationTypeQueryParameter === "free"; - } -} diff --git a/apps/web/src/app/billing/trial-initiation/trial-initiation.module.ts b/apps/web/src/app/billing/trial-initiation/trial-initiation.module.ts index 3e6bfdc4e6c..06e1cce7f23 100644 --- a/apps/web/src/app/billing/trial-initiation/trial-initiation.module.ts +++ b/apps/web/src/app/billing/trial-initiation/trial-initiation.module.ts @@ -7,36 +7,10 @@ import { FormFieldModule } from "@bitwarden/components"; import { OrganizationCreateModule } from "../../admin-console/organizations/create/organization-create.module"; import { TrialBillingStepComponent } from "../../billing/accounts/trial-initiation/trial-billing-step.component"; -import { SecretsManagerTrialFreeStepperComponent } from "../../billing/trial-initiation/secrets-manager/secrets-manager-trial-free-stepper.component"; -import { SecretsManagerTrialPaidStepperComponent } from "../../billing/trial-initiation/secrets-manager/secrets-manager-trial-paid-stepper.component"; -import { SecretsManagerTrialComponent } from "../../billing/trial-initiation/secrets-manager/secrets-manager-trial.component"; -import { EnvironmentSelectorModule } from "../../components/environment-selector/environment-selector.module"; import { SharedModule } from "../../shared"; import { CompleteTrialInitiationComponent } from "./complete-trial-initiation/complete-trial-initiation.component"; import { ConfirmationDetailsComponent } from "./confirmation-details.component"; -import { AbmEnterpriseContentComponent } from "./content/abm-enterprise-content.component"; -import { AbmTeamsContentComponent } from "./content/abm-teams-content.component"; -import { CnetEnterpriseContentComponent } from "./content/cnet-enterprise-content.component"; -import { CnetIndividualContentComponent } from "./content/cnet-individual-content.component"; -import { CnetTeamsContentComponent } from "./content/cnet-teams-content.component"; -import { DefaultContentComponent } from "./content/default-content.component"; -import { EnterpriseContentComponent } from "./content/enterprise-content.component"; -import { Enterprise1ContentComponent } from "./content/enterprise1-content.component"; -import { Enterprise2ContentComponent } from "./content/enterprise2-content.component"; -import { LogoBadgesComponent } from "./content/logo-badges.component"; -import { LogoCnet5StarsComponent } from "./content/logo-cnet-5-stars.component"; -import { LogoCnetComponent } from "./content/logo-cnet.component"; -import { LogoCompanyTestimonialComponent } from "./content/logo-company-testimonial.component"; -import { LogoForbesComponent } from "./content/logo-forbes.component"; -import { LogoUSNewsComponent } from "./content/logo-us-news.component"; -import { ReviewBlurbComponent } from "./content/review-blurb.component"; -import { ReviewLogoComponent } from "./content/review-logo.component"; -import { SecretsManagerContentComponent } from "./content/secrets-manager-content.component"; -import { TeamsContentComponent } from "./content/teams-content.component"; -import { Teams1ContentComponent } from "./content/teams1-content.component"; -import { Teams2ContentComponent } from "./content/teams2-content.component"; -import { Teams3ContentComponent } from "./content/teams3-content.component"; import { VerticalStepperModule } from "./vertical-stepper/vertical-stepper.module"; @NgModule({ @@ -46,41 +20,10 @@ import { VerticalStepperModule } from "./vertical-stepper/vertical-stepper.modul VerticalStepperModule, FormFieldModule, OrganizationCreateModule, - EnvironmentSelectorModule, TrialBillingStepComponent, InputPasswordComponent, ], - declarations: [ - CompleteTrialInitiationComponent, - EnterpriseContentComponent, - TeamsContentComponent, - ConfirmationDetailsComponent, - DefaultContentComponent, - EnterpriseContentComponent, - Enterprise1ContentComponent, - Enterprise2ContentComponent, - TeamsContentComponent, - Teams1ContentComponent, - Teams2ContentComponent, - Teams3ContentComponent, - CnetEnterpriseContentComponent, - CnetIndividualContentComponent, - CnetTeamsContentComponent, - AbmEnterpriseContentComponent, - AbmTeamsContentComponent, - LogoBadgesComponent, - LogoCnet5StarsComponent, - LogoCompanyTestimonialComponent, - LogoCnetComponent, - LogoForbesComponent, - LogoUSNewsComponent, - ReviewLogoComponent, - SecretsManagerContentComponent, - ReviewBlurbComponent, - SecretsManagerTrialComponent, - SecretsManagerTrialFreeStepperComponent, - SecretsManagerTrialPaidStepperComponent, - ], + declarations: [CompleteTrialInitiationComponent, ConfirmationDetailsComponent], exports: [CompleteTrialInitiationComponent], providers: [TitleCasePipe], }) From ef80c2370700c79a095d562bbc4cb1ab03d49a8f Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Wed, 23 Apr 2025 18:45:29 +0200 Subject: [PATCH 040/499] Fix type 0 not being blocked on key wrapping (#14388) * Fix type 0 not being blocked on key wrapping * Move block type0 below key null check --- .../encrypt.service.implementation.ts | 6 +++ .../crypto/services/encrypt.service.spec.ts | 39 +++++++++++++++++++ 2 files changed, 45 insertions(+) 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 f4318840515..fceef34421c 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 @@ -116,6 +116,12 @@ export class EncryptServiceImplementation implements EncryptService { throw new Error("No encryption key provided."); } + if (this.blockType0) { + if (key.inner().type === EncryptionType.AesCbc256_B64 || key.key.byteLength < 64) { + throw new Error("Type 0 encryption is not supported."); + } + } + if (plainValue == null) { return Promise.resolve(null); } diff --git a/libs/common/src/key-management/crypto/services/encrypt.service.spec.ts b/libs/common/src/key-management/crypto/services/encrypt.service.spec.ts index 6b2851ad116..bc945a5eff7 100644 --- a/libs/common/src/key-management/crypto/services/encrypt.service.spec.ts +++ b/libs/common/src/key-management/crypto/services/encrypt.service.spec.ts @@ -55,6 +55,19 @@ describe("EncryptService", () => { "No wrappingKey provided for wrapping.", ); }); + it("fails if type 0 key is provided with flag turned on", async () => { + (encryptService as any).blockType0 = true; + const mock32Key = mock(); + mock32Key.key = makeStaticByteArray(32); + mock32Key.inner.mockReturnValue({ + type: 0, + encryptionKey: mock32Key.key, + }); + + await expect(encryptService.wrapSymmetricKey(mock32Key, mock32Key)).rejects.toThrow( + "Type 0 encryption is not supported.", + ); + }); }); describe("wrapDecapsulationKey", () => { @@ -83,6 +96,19 @@ describe("EncryptService", () => { "No wrappingKey provided for wrapping.", ); }); + it("throws if type 0 key is provided with flag turned on", async () => { + (encryptService as any).blockType0 = true; + const mock32Key = mock(); + mock32Key.key = makeStaticByteArray(32); + mock32Key.inner.mockReturnValue({ + type: 0, + encryptionKey: mock32Key.key, + }); + + await expect( + encryptService.wrapDecapsulationKey(new Uint8Array(200), mock32Key), + ).rejects.toThrow("Type 0 encryption is not supported."); + }); }); describe("wrapEncapsulationKey", () => { @@ -111,6 +137,19 @@ describe("EncryptService", () => { "No wrappingKey provided for wrapping.", ); }); + it("throws if type 0 key is provided with flag turned on", async () => { + (encryptService as any).blockType0 = true; + const mock32Key = mock(); + mock32Key.key = makeStaticByteArray(32); + mock32Key.inner.mockReturnValue({ + type: 0, + encryptionKey: mock32Key.key, + }); + + await expect( + encryptService.wrapEncapsulationKey(new Uint8Array(200), mock32Key), + ).rejects.toThrow("Type 0 encryption is not supported."); + }); }); describe("onServerConfigChange", () => { From b589951c907eb23cc0e2c0711979e3619e1d1e59 Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Wed, 23 Apr 2025 11:13:44 -0700 Subject: [PATCH 041/499] [PM-18520] - Update desktop cipher forms to use the same UI as web app and extension - (#13992) * WIP - cipher form refactor * cipher clone * cipher clone * finalize item view and form changes * fix tests * hide changes behind feature flag * set flag to false * create vault items v2. add button selector * revert change to flag and vault items * add attachments * revert change to tsconfig * move module * fix modules * cleanup * fix import * fix import * fix import * remove showForm * update feature flag * wip - cleanup * fix up services * cleanup * fix type errors * fix lint errors * add dialog component * revert changes to menu * revert changes to menu * fix vault-items-v2 * set feature flag to FALSE * add missing i18n keys. fix collection state * remove generator. update modules. bug fix * fix restricted imports * mark method as deprecated. add uri arg back * fix shared.module * fix shared.module * fix shared.module * add uri * check and prompt for premium when opening attachments dialog * move VaultItemDialogResult back * fix import in spec file * update copy functions * fix MP reprompt issue --- .../browser-view-password-history.service.ts | 2 - apps/desktop/src/app/app-routing.module.ts | 19 +- apps/desktop/src/app/app.module.ts | 8 +- apps/desktop/src/locales/en/messages.json | 142 ++++ apps/desktop/src/scss/base.scss | 5 + apps/desktop/src/scss/vault.scss | 4 + .../desktop-cipher-form-generator.service.ts | 32 + ...top-premium-upgrade-prompt.service.spec.ts | 30 + .../desktop-premium-upgrade-prompt.service.ts | 15 + ...credential-generator-dialog.component.html | 2 +- .../credential-generator-dialog.component.ts | 37 +- .../app/vault/item-footer.component.html | 64 ++ .../vault/app/vault/item-footer.component.ts | 159 ++++ .../app/vault/vault-items-v2.component.html | 92 ++ .../app/vault/vault-items-v2.component.ts | 42 + .../vault/app/vault/vault-v2.component.html | 80 ++ .../src/vault/app/vault/vault-v2.component.ts | 785 ++++++++++++++++++ .../collections/vault.component.ts | 6 +- .../view/emergency-view-dialog.component.ts | 5 +- .../vault-item-dialog.component.ts | 14 +- .../individual-vault/add-edit-v2.component.ts | 3 +- .../vault/individual-vault/vault.component.ts | 8 +- .../vault/individual-vault/view.component.ts | 8 +- .../view-password-history.service.spec.ts | 15 +- .../services/view-password-history.service.ts | 7 +- .../vault/components/vault-items.component.ts | 8 +- libs/common/src/enums/feature-flag.enum.ts | 2 + .../components/identity/identity.component.ts | 32 +- .../attachments-v2.component.html | 0 .../attachments-v2.component.spec.ts | 0 .../attachments}/attachments-v2.component.ts | 14 +- .../src/cipher-view/cipher-view.component.ts | 3 +- libs/vault/src/cipher-view/index.ts | 1 + .../password-history.component.html | 0 .../password-history.component.ts | 13 +- libs/vault/src/index.ts | 1 + 36 files changed, 1569 insertions(+), 89 deletions(-) create mode 100644 apps/desktop/src/services/desktop-cipher-form-generator.service.ts create mode 100644 apps/desktop/src/services/desktop-premium-upgrade-prompt.service.spec.ts create mode 100644 apps/desktop/src/services/desktop-premium-upgrade-prompt.service.ts create mode 100644 apps/desktop/src/vault/app/vault/item-footer.component.html create mode 100644 apps/desktop/src/vault/app/vault/item-footer.component.ts create mode 100644 apps/desktop/src/vault/app/vault/vault-items-v2.component.html create mode 100644 apps/desktop/src/vault/app/vault/vault-items-v2.component.ts create mode 100644 apps/desktop/src/vault/app/vault/vault-v2.component.html create mode 100644 apps/desktop/src/vault/app/vault/vault-v2.component.ts rename apps/web/src/app/vault/services/web-view-password-history.service.spec.ts => libs/angular/src/services/view-password-history.service.spec.ts (69%) rename apps/web/src/app/vault/services/web-view-password-history.service.ts => libs/angular/src/services/view-password-history.service.ts (78%) rename {apps/web/src/app/vault/individual-vault => libs/vault/src/cipher-view/attachments}/attachments-v2.component.html (100%) rename {apps/web/src/app/vault/individual-vault => libs/vault/src/cipher-view/attachments}/attachments-v2.component.spec.ts (100%) rename {apps/web/src/app/vault/individual-vault => libs/vault/src/cipher-view/attachments}/attachments-v2.component.ts (84%) rename {apps/web/src/app/vault/individual-vault => libs/vault/src/components/password-history}/password-history.component.html (100%) rename {apps/web/src/app/vault/individual-vault => libs/vault/src/components/password-history}/password-history.component.ts (95%) diff --git a/apps/browser/src/vault/popup/services/browser-view-password-history.service.ts b/apps/browser/src/vault/popup/services/browser-view-password-history.service.ts index 5e400da9de5..ae6369d06a5 100644 --- a/apps/browser/src/vault/popup/services/browser-view-password-history.service.ts +++ b/apps/browser/src/vault/popup/services/browser-view-password-history.service.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { inject } from "@angular/core"; import { Router } from "@angular/router"; diff --git a/apps/desktop/src/app/app-routing.module.ts b/apps/desktop/src/app/app-routing.module.ts index 0c6bc730c2c..00463152a95 100644 --- a/apps/desktop/src/app/app-routing.module.ts +++ b/apps/desktop/src/app/app-routing.module.ts @@ -14,6 +14,7 @@ import { tdeDecryptionRequiredGuard, unauthGuardFn, } from "@bitwarden/angular/auth/guards"; +import { featureFlaggedRoute } from "@bitwarden/angular/platform/utils/feature-flagged-route"; import { NewDeviceVerificationNoticeGuard } from "@bitwarden/angular/vault/guards"; import { AnonLayoutWrapperComponent, @@ -41,6 +42,7 @@ import { NewDeviceVerificationComponent, DeviceVerificationIcon, } from "@bitwarden/auth/angular"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { LockComponent } from "@bitwarden/key-management-ui"; import { NewDeviceVerificationNoticePageOneComponent, @@ -53,6 +55,7 @@ import { maxAccountsGuardFn } from "../auth/guards/max-accounts.guard"; import { SetPasswordComponent } from "../auth/set-password.component"; import { UpdateTempPasswordComponent } from "../auth/update-temp-password.component"; import { RemovePasswordComponent } from "../key-management/key-connector/remove-password.component"; +import { VaultV2Component } from "../vault/app/vault/vault-v2.component"; import { VaultComponent } from "../vault/app/vault/vault.component"; import { Fido2PlaceholderComponent } from "./components/fido2placeholder.component"; @@ -132,11 +135,15 @@ const routes: Routes = [ }, ], }, - { - path: "vault", - component: VaultComponent, - canActivate: [authGuard, NewDeviceVerificationNoticeGuard], - }, + ...featureFlaggedRoute({ + defaultComponent: VaultComponent, + flaggedComponent: VaultV2Component, + featureFlag: FeatureFlag.PM18520_UpdateDesktopCipherForm, + routeOptions: { + path: "vault", + canActivate: [authGuard, NewDeviceVerificationNoticeGuard], + }, + }), { path: "accessibility-cookie", component: AccessibilityCookieComponent }, { path: "set-password", component: SetPasswordComponent }, { @@ -359,7 +366,7 @@ const routes: Routes = [ imports: [ RouterModule.forRoot(routes, { useHash: true, - /*enableTracing: true,*/ + // enableTracing: true, }), ], exports: [RouterModule], diff --git a/apps/desktop/src/app/app.module.ts b/apps/desktop/src/app/app.module.ts index c84f1a96afd..15ab4350bbc 100644 --- a/apps/desktop/src/app/app.module.ts +++ b/apps/desktop/src/app/app.module.ts @@ -9,7 +9,6 @@ import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; import { ColorPasswordCountPipe } from "@bitwarden/angular/pipes/color-password-count.pipe"; import { ColorPasswordPipe } from "@bitwarden/angular/pipes/color-password.pipe"; import { CalloutModule, DialogModule } from "@bitwarden/components"; -import { DecryptionFailureDialogComponent } from "@bitwarden/vault"; import { AccessibilityCookieComponent } from "../auth/accessibility-cookie.component"; import { DeleteAccountComponent } from "../auth/delete-account.component"; @@ -28,6 +27,7 @@ import { PasswordHistoryComponent } from "../vault/app/vault/password-history.co import { ShareComponent } from "../vault/app/vault/share.component"; import { VaultFilterModule } from "../vault/app/vault/vault-filter/vault-filter.module"; import { VaultItemsComponent } from "../vault/app/vault/vault-items.component"; +import { VaultV2Component } from "../vault/app/vault/vault-v2.component"; import { VaultComponent } from "../vault/app/vault/vault.component"; import { ViewCustomFieldsComponent } from "../vault/app/vault/view-custom-fields.component"; import { ViewComponent } from "../vault/app/vault/view.component"; @@ -55,8 +55,8 @@ import { SharedModule } from "./shared/shared.module"; CalloutModule, DeleteAccountComponent, UserVerificationComponent, - DecryptionFailureDialogComponent, NavComponent, + VaultV2Component, ], declarations: [ AccessibilityCookieComponent, @@ -65,7 +65,6 @@ import { SharedModule } from "./shared/shared.module"; AddEditCustomFieldsComponent, AppComponent, AttachmentsComponent, - VaultItemsComponent, CollectionsComponent, ColorPasswordPipe, ColorPasswordCountPipe, @@ -80,9 +79,10 @@ import { SharedModule } from "./shared/shared.module"; ShareComponent, UpdateTempPasswordComponent, VaultComponent, + VaultItemsComponent, VaultTimeoutInputComponent, - ViewComponent, ViewCustomFieldsComponent, + ViewComponent, ], providers: [SshAgentService], bootstrap: [AppComponent], diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index 6097543e50a..81e3a94ff4d 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -393,6 +393,64 @@ "authenticatorKeyTotp": { "message": "Authenticator key (TOTP)" }, + "authenticatorKey": { + "message": "Authenticator key" + }, + "autofillOptions": { + "message": "Autofill options" + }, + "websiteUri": { + "message": "Website (URI)" + }, + "websiteUriCount": { + "message": "Website (URI) $COUNT$", + "description": "Label for an input field that contains a website URI. The input field is part of a list of fields, and the count indicates the position of the field in the list.", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "websiteAdded": { + "message": "Website added" + }, + "addWebsite": { + "message": "Add website" + }, + "deleteWebsite": { + "message": "Delete website" + }, + "owner": { + "message": "Owner" + }, + "addField": { + "message": "Add field" + }, + "fieldType": { + "message": "Field type" + }, + "fieldLabel": { + "message": "Field label" + }, + "add": { + "message": "Add" + }, + "textHelpText": { + "message": "Use text fields for data like security questions" + }, + "hiddenHelpText": { + "message": "Use hidden fields for sensitive data like a password" + }, + "checkBoxHelpText": { + "message": "Use checkboxes if you'd like to autofill a form's checkbox, like a remember email" + }, + "linkedHelpText": { + "message": "Use a linked field when you are experiencing autofill issues for a specific website." + }, + "linkedLabelHelpText": { + "message": "Enter the the field's html id, name, aria-label, or placeholder." + }, "folder": { "message": "Folder" }, @@ -418,6 +476,9 @@ "message": "Linked", "description": "This describes a field that is 'linked' (related) to another field." }, + "cfTypeCheckbox": { + "message": "Checkbox" + }, "linkedValue": { "message": "Linked value", "description": "This describes a value that is 'linked' (related) to another value." @@ -1915,6 +1976,43 @@ } } }, + "cardDetails": { + "message": "Card details" + }, + "cardBrandDetails": { + "message": "$BRAND$ details", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } + }, + "learnMoreAboutAuthenticators": { + "message": "Learn more about authenticators" + }, + "copyTOTP": { + "message": "Copy Authenticator key (TOTP)" + }, + "totpHelperTitle": { + "message": "Make 2-step verification seamless" + }, + "totpHelper": { + "message": "Bitwarden can store and fill 2-step verification codes. Copy and paste the key into this field." + }, + "totpHelperWithCapture": { + "message": "Bitwarden can store and fill 2-step verification codes. Select the camera icon to take a screenshot of this website's authenticator QR code, or copy and paste the key into this field." + }, + "premium": { + "message": "Premium", + "description": "Premium membership" + }, + "cardExpiredTitle": { + "message": "Expired card" + }, + "cardExpiredMessage": { + "message": "If you've renewed it, update the card's information" + }, "verificationRequired": { "message": "Verification required", "description": "Default title for the user verification dialog." @@ -2084,6 +2182,15 @@ "personalOwnershipPolicyInEffectImports": { "message": "An organization policy has blocked importing items into your individual vault." }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact information" + }, "allSends": { "message": "All Sends", "description": "'Sends' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2518,6 +2625,9 @@ "generateEmail": { "message": "Generate email" }, + "usernameGenerator": { + "message": "Username generator" + }, "spinboxBoundariesHint": { "message": "Value must be between $MIN$ and $MAX$.", "description": "Explains spin box minimum and maximum values to the user", @@ -3439,6 +3549,17 @@ "ssoError": { "message": "No free ports could be found for the sso login." }, + "securePasswordGenerated": { + "message": "Secure password generated! Don't forget to also update your password on the website." + }, + "useGeneratorHelpTextPartOne": { + "message": "Use the generator", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, + "useGeneratorHelpTextPartTwo": { + "message": "to create a strong unique password", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, "biometricsStatusHelptextUnlockNeeded": { "message": "Biometric unlock is unavailable because PIN or password unlock is required first." }, @@ -3517,6 +3638,27 @@ "setupTwoStepLogin": { "message": "Set up two-step login" }, + "itemDetails": { + "message": "Item details" + }, + "itemName": { + "message": "Item name" + }, + "loginCredentials": { + "message": "Login credentials" + }, + "additionalOptions": { + "message": "Additional options" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "upload": { + "message": "Upload" + }, "newDeviceVerificationNoticeContentPage1": { "message": "Bitwarden will send a code to your account email to verify logins from new devices starting in February 2025." }, diff --git a/apps/desktop/src/scss/base.scss b/apps/desktop/src/scss/base.scss index 22eb3df0d17..494e91529ee 100644 --- a/apps/desktop/src/scss/base.scss +++ b/apps/desktop/src/scss/base.scss @@ -147,3 +147,8 @@ div:not(.modal)::-webkit-scrollbar-thumb, .mx-auto { margin-left: auto !important; } + +.vault-v2 button:not([bitbutton]):not([biticonbutton]) i.bwi, +a i.bwi { + margin-right: 0.25rem; +} diff --git a/apps/desktop/src/scss/vault.scss b/apps/desktop/src/scss/vault.scss index f7403ad62d2..88216a2b926 100644 --- a/apps/desktop/src/scss/vault.scss +++ b/apps/desktop/src/scss/vault.scss @@ -162,3 +162,7 @@ app-root { } } } + +.vault-v2 > .details { + flex-direction: column-reverse; +} diff --git a/apps/desktop/src/services/desktop-cipher-form-generator.service.ts b/apps/desktop/src/services/desktop-cipher-form-generator.service.ts new file mode 100644 index 00000000000..8a33f4ced0a --- /dev/null +++ b/apps/desktop/src/services/desktop-cipher-form-generator.service.ts @@ -0,0 +1,32 @@ +import { inject, Injectable } from "@angular/core"; +import { firstValueFrom } from "rxjs"; + +import { DialogService } from "@bitwarden/components"; +import { CipherFormGenerationService } from "@bitwarden/vault"; + +import { CredentialGeneratorDialogComponent } from "../vault/app/vault/credential-generator-dialog.component"; + +@Injectable() +export class DesktopCredentialGenerationService implements CipherFormGenerationService { + private dialogService = inject(DialogService); + + async generatePassword(): Promise { + return await this.generateCredential("password"); + } + + async generateUsername(uri: string): Promise { + return await this.generateCredential("username", uri); + } + + async generateCredential(type: "password" | "username", uri?: string): Promise { + const dialogRef = CredentialGeneratorDialogComponent.open(this.dialogService, { type, uri }); + + const result = await firstValueFrom(dialogRef.closed); + + if (!result || result.action === "canceled" || !result.generatedValue) { + return ""; + } + + return result.generatedValue; + } +} diff --git a/apps/desktop/src/services/desktop-premium-upgrade-prompt.service.spec.ts b/apps/desktop/src/services/desktop-premium-upgrade-prompt.service.spec.ts new file mode 100644 index 00000000000..3b33116ea5a --- /dev/null +++ b/apps/desktop/src/services/desktop-premium-upgrade-prompt.service.spec.ts @@ -0,0 +1,30 @@ +import { TestBed } from "@angular/core/testing"; +import { mock, MockProxy } from "jest-mock-extended"; + +import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; + +import { DesktopPremiumUpgradePromptService } from "./desktop-premium-upgrade-prompt.service"; + +describe("DesktopPremiumUpgradePromptService", () => { + let service: DesktopPremiumUpgradePromptService; + let messager: MockProxy; + + beforeEach(async () => { + messager = mock(); + await TestBed.configureTestingModule({ + providers: [ + DesktopPremiumUpgradePromptService, + { provide: MessagingService, useValue: messager }, + ], + }).compileComponents(); + + service = TestBed.inject(DesktopPremiumUpgradePromptService); + }); + + describe("promptForPremium", () => { + it("navigates to the premium update screen", async () => { + await service.promptForPremium(); + expect(messager.send).toHaveBeenCalledWith("openPremium"); + }); + }); +}); diff --git a/apps/desktop/src/services/desktop-premium-upgrade-prompt.service.ts b/apps/desktop/src/services/desktop-premium-upgrade-prompt.service.ts new file mode 100644 index 00000000000..f2375ecfebb --- /dev/null +++ b/apps/desktop/src/services/desktop-premium-upgrade-prompt.service.ts @@ -0,0 +1,15 @@ +import { inject } from "@angular/core"; + +import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; +import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service"; + +/** + * This class handles the premium upgrade process for the desktop. + */ +export class DesktopPremiumUpgradePromptService implements PremiumUpgradePromptService { + private messagingService = inject(MessagingService); + + async promptForPremium() { + this.messagingService.send("openPremium"); + } +} diff --git a/apps/desktop/src/vault/app/vault/credential-generator-dialog.component.html b/apps/desktop/src/vault/app/vault/credential-generator-dialog.component.html index 47232dff66d..31f47d824d6 100644 --- a/apps/desktop/src/vault/app/vault/credential-generator-dialog.component.html +++ b/apps/desktop/src/vault/app/vault/credential-generator-dialog.component.html @@ -3,6 +3,7 @@ @@ -27,7 +28,6 @@ (click)="applyCredentials()" appA11yTitle="{{ buttonLabel }}" bitButton - bitDialogClose [disabled]="!(buttonLabel && credentialValue)" > {{ buttonLabel }} diff --git a/apps/desktop/src/vault/app/vault/credential-generator-dialog.component.ts b/apps/desktop/src/vault/app/vault/credential-generator-dialog.component.ts index eda35a8c76d..2858d7330e5 100644 --- a/apps/desktop/src/vault/app/vault/credential-generator-dialog.component.ts +++ b/apps/desktop/src/vault/app/vault/credential-generator-dialog.component.ts @@ -10,6 +10,7 @@ import { DialogService, ItemModule, LinkModule, + DialogRef, } from "@bitwarden/components"; import { CredentialGeneratorHistoryDialogComponent, @@ -19,10 +20,22 @@ import { AlgorithmInfo } from "@bitwarden/generator-core"; import { CipherFormGeneratorComponent } from "@bitwarden/vault"; type CredentialGeneratorParams = { - onCredentialGenerated: (value?: string) => void; + /** @deprecated Prefer use of dialogRef.closed to retreive the generated value */ + onCredentialGenerated?: (value?: string) => void; type: "password" | "username"; + uri?: string; }; +export interface CredentialGeneratorDialogResult { + action: CredentialGeneratorDialogAction; + generatedValue?: string; +} + +export enum CredentialGeneratorDialogAction { + Selected = "selected", + Canceled = "canceled", +} + @Component({ standalone: true, selector: "credential-generator-dialog", @@ -45,6 +58,7 @@ export class CredentialGeneratorDialogComponent { constructor( @Inject(DIALOG_DATA) protected data: CredentialGeneratorParams, private dialogService: DialogService, + private dialogRef: DialogRef, private i18nService: I18nService, ) {} @@ -59,11 +73,15 @@ export class CredentialGeneratorDialogComponent { }; applyCredentials = () => { - this.data.onCredentialGenerated(this.credentialValue); + this.data.onCredentialGenerated?.(this.credentialValue); + this.dialogRef.close({ + action: CredentialGeneratorDialogAction.Selected, + generatedValue: this.credentialValue, + }); }; clearCredentials = () => { - this.data.onCredentialGenerated(); + this.data.onCredentialGenerated?.(); }; onCredentialGenerated = (value: string) => { @@ -75,9 +93,12 @@ export class CredentialGeneratorDialogComponent { this.dialogService.open(CredentialGeneratorHistoryDialogComponent); }; - static open = (dialogService: DialogService, data: CredentialGeneratorParams) => { - dialogService.open(CredentialGeneratorDialogComponent, { - data, - }); - }; + static open(dialogService: DialogService, data: CredentialGeneratorParams) { + return dialogService.open( + CredentialGeneratorDialogComponent, + { + data, + }, + ); + } } diff --git a/apps/desktop/src/vault/app/vault/item-footer.component.html b/apps/desktop/src/vault/app/vault/item-footer.component.html new file mode 100644 index 00000000000..6915555c08b --- /dev/null +++ b/apps/desktop/src/vault/app/vault/item-footer.component.html @@ -0,0 +1,64 @@ + diff --git a/apps/desktop/src/vault/app/vault/item-footer.component.ts b/apps/desktop/src/vault/app/vault/item-footer.component.ts new file mode 100644 index 00000000000..639d1557ecd --- /dev/null +++ b/apps/desktop/src/vault/app/vault/item-footer.component.ts @@ -0,0 +1,159 @@ +import { CommonModule } from "@angular/common"; +import { Input, Output, EventEmitter, Component, OnInit, ViewChild } from "@angular/core"; +import { Observable, 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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { CollectionId, UserId } from "@bitwarden/common/types/guid"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { CipherRepromptType } 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 { ButtonComponent, ButtonModule, DialogService, ToastService } from "@bitwarden/components"; +import { PasswordRepromptService } from "@bitwarden/vault"; + +@Component({ + selector: "app-vault-item-footer", + templateUrl: "item-footer.component.html", + standalone: true, + imports: [ButtonModule, CommonModule, JslibModule], +}) +export class ItemFooterComponent implements OnInit { + @Input({ required: true }) cipher: CipherView = new CipherView(); + @Input() collectionId: string | null = null; + @Input({ required: true }) action: string = "view"; + @Input() isSubmitting: boolean = false; + @Output() onEdit = new EventEmitter(); + @Output() onClone = new EventEmitter(); + @Output() onDelete = new EventEmitter(); + @Output() onRestore = new EventEmitter(); + @Output() onCancel = new EventEmitter(); + @ViewChild("submitBtn", { static: false }) submitBtn: ButtonComponent | null = null; + + canDeleteCipher$: Observable = new Observable(); + activeUserId: UserId | null = null; + + private passwordReprompted = false; + + constructor( + protected cipherService: CipherService, + protected dialogService: DialogService, + protected passwordRepromptService: PasswordRepromptService, + protected cipherAuthorizationService: CipherAuthorizationService, + protected accountService: AccountService, + protected toastService: ToastService, + protected i18nService: I18nService, + protected logService: LogService, + ) {} + + async ngOnInit() { + this.canDeleteCipher$ = this.cipherAuthorizationService.canDeleteCipher$(this.cipher, [ + this.collectionId as CollectionId, + ]); + this.activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + } + + async clone() { + if (this.cipher.login?.hasFido2Credentials) { + const confirmed = await this.dialogService.openSimpleDialog({ + title: { key: "passkeyNotCopied" }, + content: { key: "passkeyNotCopiedAlert" }, + type: "info", + }); + + if (!confirmed) { + return false; + } + } + + if (await this.promptPassword()) { + this.onClone.emit(this.cipher); + return true; + } + + return false; + } + + protected edit() { + this.onEdit.emit(this.cipher); + } + + cancel() { + this.onCancel.emit(this.cipher); + } + + async delete(): Promise { + if (!(await this.promptPassword())) { + return false; + } + + const confirmed = await this.dialogService.openSimpleDialog({ + title: { key: "deleteItem" }, + content: { + key: this.cipher.isDeleted ? "permanentlyDeleteItemConfirmation" : "deleteItemConfirmation", + }, + type: "warning", + }); + + if (!confirmed) { + return false; + } + + try { + const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + await this.deleteCipher(activeUserId); + this.toastService.showToast({ + variant: "success", + message: this.i18nService.t( + this.cipher.isDeleted ? "permanentlyDeletedItem" : "deletedItem", + ), + }); + this.onDelete.emit(this.cipher); + } catch (e) { + this.logService.error(e); + } + + return true; + } + + async restore(): Promise { + if (!this.cipher.isDeleted) { + return false; + } + + try { + const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + await this.restoreCipher(activeUserId); + this.toastService.showToast({ + variant: "success", + message: this.i18nService.t("restoredItem"), + }); + this.onRestore.emit(this.cipher); + } catch (e) { + this.logService.error(e); + } + + return true; + } + + protected deleteCipher(userId: UserId) { + return this.cipher.isDeleted + ? this.cipherService.deleteWithServer(this.cipher.id, userId) + : this.cipherService.softDeleteWithServer(this.cipher.id, userId); + } + + protected restoreCipher(userId: UserId) { + return this.cipherService.restoreWithServer(this.cipher.id, userId); + } + + protected async promptPassword() { + if (this.cipher.reprompt === CipherRepromptType.None || this.passwordReprompted) { + return true; + } + + return (this.passwordReprompted = await this.passwordRepromptService.showPasswordPrompt()); + } +} diff --git a/apps/desktop/src/vault/app/vault/vault-items-v2.component.html b/apps/desktop/src/vault/app/vault/vault-items-v2.component.html new file mode 100644 index 00000000000..ff35e00fb0f --- /dev/null +++ b/apps/desktop/src/vault/app/vault/vault-items-v2.component.html @@ -0,0 +1,92 @@ +
+ +
+ +
+ +
+ +
+
+
+ +

{{ "noItemsInList" | i18n }}

+ +
+ +
+
+ + + + + + + + + + diff --git a/apps/desktop/src/vault/app/vault/vault-items-v2.component.ts b/apps/desktop/src/vault/app/vault/vault-items-v2.component.ts new file mode 100644 index 00000000000..31d4098d2b2 --- /dev/null +++ b/apps/desktop/src/vault/app/vault/vault-items-v2.component.ts @@ -0,0 +1,42 @@ +import { ScrollingModule } from "@angular/cdk/scrolling"; +import { CommonModule } from "@angular/common"; +import { Component } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { distinctUntilChanged } from "rxjs"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { VaultItemsComponent as BaseVaultItemsComponent } from "@bitwarden/angular/vault/components/vault-items.component"; +import { SearchService } from "@bitwarden/common/abstractions/search.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { MenuModule } from "@bitwarden/components"; + +import { SearchBarService } from "../../../app/layout/search/search-bar.service"; + +@Component({ + selector: "app-vault-items-v2", + templateUrl: "vault-items-v2.component.html", + standalone: true, + imports: [MenuModule, CommonModule, JslibModule, ScrollingModule], +}) +export class VaultItemsV2Component extends BaseVaultItemsComponent { + constructor( + searchService: SearchService, + private readonly searchBarService: SearchBarService, + cipherService: CipherService, + accountService: AccountService, + ) { + super(searchService, cipherService, accountService); + + this.searchBarService.searchText$ + .pipe(distinctUntilChanged(), takeUntilDestroyed()) + .subscribe((searchText) => { + this.searchText = searchText!; + }); + } + + trackByFn(index: number, c: CipherView): string { + return c.id; + } +} diff --git a/apps/desktop/src/vault/app/vault/vault-v2.component.html b/apps/desktop/src/vault/app/vault/vault-v2.component.html new file mode 100644 index 00000000000..12f52502984 --- /dev/null +++ b/apps/desktop/src/vault/app/vault/vault-v2.component.html @@ -0,0 +1,80 @@ +
+ + +
+ +
+
+
+ + + + + + + +
+
+
+
+ +
+ + +
+
+ diff --git a/apps/desktop/src/vault/app/vault/vault-v2.component.ts b/apps/desktop/src/vault/app/vault/vault-v2.component.ts new file mode 100644 index 00000000000..7e799899418 --- /dev/null +++ b/apps/desktop/src/vault/app/vault/vault-v2.component.ts @@ -0,0 +1,785 @@ +import { CommonModule } from "@angular/common"; +import { + ChangeDetectorRef, + Component, + NgZone, + OnDestroy, + OnInit, + ViewChild, + ViewContainerRef, +} from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { ActivatedRoute, Router } from "@angular/router"; +import { firstValueFrom, Subject, takeUntil, switchMap } from "rxjs"; +import { filter, map, take } from "rxjs/operators"; + +import { CollectionView } from "@bitwarden/admin-console/common"; +import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref"; +import { ModalService } from "@bitwarden/angular/services/modal.service"; +import { VaultViewPasswordHistoryService } from "@bitwarden/angular/services/view-password-history.service"; +import { VaultFilter } from "@bitwarden/angular/vault/vault-filter/models/vault-filter.model"; +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; +import { Account, 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"; +import { EventType } from "@bitwarden/common/enums"; +import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.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/platform/sync"; +import { CipherId, UserId } from "@bitwarden/common/types/guid"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service"; +import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service"; +import { ViewPasswordHistoryService } from "@bitwarden/common/vault/abstractions/view-password-history.service"; +import { CipherType } from "@bitwarden/common/vault/enums"; +import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; +import { + BadgeModule, + ButtonModule, + DialogService, + ItemModule, + ToastService, +} from "@bitwarden/components"; +import { I18nPipe } from "@bitwarden/ui-common"; +import { + AttachmentDialogResult, + AttachmentsV2Component, + ChangeLoginPasswordService, + CipherFormConfig, + CipherFormConfigService, + CipherFormGenerationService, + CipherFormMode, + CipherFormModule, + CipherViewComponent, + DecryptionFailureDialogComponent, + DefaultChangeLoginPasswordService, + DefaultCipherFormConfigService, + PasswordRepromptService, +} from "@bitwarden/vault"; + +import { NavComponent } from "../../../app/layout/nav.component"; +import { SearchBarService } from "../../../app/layout/search/search-bar.service"; +import { DesktopCredentialGenerationService } from "../../../services/desktop-cipher-form-generator.service"; +import { DesktopPremiumUpgradePromptService } from "../../../services/desktop-premium-upgrade-prompt.service"; +import { invokeMenu, RendererMenuItem } from "../../../utils"; + +import { FolderAddEditComponent } from "./folder-add-edit.component"; +import { ItemFooterComponent } from "./item-footer.component"; +import { VaultFilterComponent } from "./vault-filter/vault-filter.component"; +import { VaultFilterModule } from "./vault-filter/vault-filter.module"; +import { VaultItemsV2Component } from "./vault-items-v2.component"; + +const BroadcasterSubscriptionId = "VaultComponent"; + +@Component({ + selector: "app-vault", + templateUrl: "vault-v2.component.html", + standalone: true, + imports: [ + BadgeModule, + CommonModule, + CipherFormModule, + CipherViewComponent, + ItemFooterComponent, + I18nPipe, + ItemModule, + ButtonModule, + NavComponent, + VaultFilterModule, + VaultItemsV2Component, + ], + providers: [ + { + provide: CipherFormConfigService, + useClass: DefaultCipherFormConfigService, + }, + { + provide: ChangeLoginPasswordService, + useClass: DefaultChangeLoginPasswordService, + }, + { + provide: ViewPasswordHistoryService, + useClass: VaultViewPasswordHistoryService, + }, + { + provide: PremiumUpgradePromptService, + useClass: DesktopPremiumUpgradePromptService, + }, + { provide: CipherFormGenerationService, useClass: DesktopCredentialGenerationService }, + ], +}) +export class VaultV2Component implements OnInit, OnDestroy { + @ViewChild(VaultItemsV2Component, { static: true }) + vaultItemsComponent: VaultItemsV2Component | null = null; + @ViewChild(VaultFilterComponent, { static: true }) + vaultFilterComponent: VaultFilterComponent | null = null; + @ViewChild("folderAddEdit", { read: ViewContainerRef, static: true }) + folderAddEditModalRef: ViewContainerRef | null = null; + + action: CipherFormMode | "view" | null = null; + cipherId: string | null = null; + favorites = false; + type: CipherType | null = null; + folderId: string | null = null; + collectionId: string | null = null; + organizationId: string | null = null; + myVaultOnly = false; + addType: CipherType | undefined = undefined; + addOrganizationId: string | null = null; + addCollectionIds: string[] | null = null; + showingModal = false; + deleted = false; + userHasPremiumAccess = false; + activeFilter: VaultFilter = new VaultFilter(); + activeUserId: UserId | null = null; + cipherRepromptId: string | null = null; + cipher: CipherView | null = new CipherView(); + collections: CollectionView[] | null = null; + config: CipherFormConfig | null = null; + + protected canAccessAttachments$ = this.accountService.activeAccount$.pipe( + filter((account): account is Account => !!account), + switchMap((account) => + this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id), + ), + ); + + private modal: ModalRef | null = null; + private componentIsDestroyed$ = new Subject(); + + constructor( + private route: ActivatedRoute, + private router: Router, + private i18nService: I18nService, + private modalService: ModalService, + private broadcasterService: BroadcasterService, + private changeDetectorRef: ChangeDetectorRef, + private ngZone: NgZone, + private syncService: SyncService, + private messagingService: MessagingService, + private platformUtilsService: PlatformUtilsService, + private eventCollectionService: EventCollectionService, + private totpService: TotpService, + private passwordRepromptService: PasswordRepromptService, + private searchBarService: SearchBarService, + private apiService: ApiService, + private dialogService: DialogService, + private billingAccountProfileStateService: BillingAccountProfileStateService, + private toastService: ToastService, + private accountService: AccountService, + private cipherService: CipherService, + private formConfigService: CipherFormConfigService, + private premiumUpgradePromptService: PremiumUpgradePromptService, + ) {} + + async ngOnInit() { + this.accountService.activeAccount$ + .pipe( + filter((account): account is Account => !!account), + switchMap((account) => + this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id), + ), + takeUntil(this.componentIsDestroyed$), + ) + .subscribe((canAccessPremium: boolean) => { + this.userHasPremiumAccess = canAccessPremium; + }); + + this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => { + this.ngZone + .run(async () => { + let detectChanges = true; + try { + switch (message.command) { + case "newLogin": + await this.addCipher(CipherType.Login).catch(() => {}); + break; + case "newCard": + await this.addCipher(CipherType.Card).catch(() => {}); + break; + case "newIdentity": + await this.addCipher(CipherType.Identity).catch(() => {}); + break; + case "newSecureNote": + await this.addCipher(CipherType.SecureNote).catch(() => {}); + break; + case "focusSearch": + (document.querySelector("#search") as HTMLInputElement)?.select(); + detectChanges = false; + break; + case "syncCompleted": + if (this.vaultItemsComponent) { + await this.vaultItemsComponent + .reload(this.activeFilter.buildFilter()) + .catch(() => {}); + } + if (this.vaultFilterComponent) { + await this.vaultFilterComponent + .reloadCollectionsAndFolders(this.activeFilter) + .catch(() => {}); + await this.vaultFilterComponent.reloadOrganizations().catch(() => {}); + } + break; + case "modalShown": + this.showingModal = true; + break; + case "modalClosed": + this.showingModal = false; + break; + case "copyUsername": { + if (this.cipher?.login?.username) { + this.copyValue(this.cipher, this.cipher?.login?.username, "username", "Username"); + } + break; + } + case "copyPassword": { + if (this.cipher?.login?.password && this.cipher.viewPassword) { + this.copyValue(this.cipher, this.cipher.login.password, "password", "Password"); + await this.eventCollectionService + .collect(EventType.Cipher_ClientCopiedPassword, this.cipher.id) + .catch(() => {}); + } + break; + } + case "copyTotp": { + if ( + this.cipher?.login?.hasTotp && + (this.cipher.organizationUseTotp || this.userHasPremiumAccess) + ) { + const value = await firstValueFrom( + this.totpService.getCode$(this.cipher.login.totp), + ).catch(() => null); + if (value) { + this.copyValue(this.cipher, value.code, "verificationCodeTotp", "TOTP"); + } + } + break; + } + default: + detectChanges = false; + break; + } + } catch { + // Ignore errors + } + if (detectChanges) { + this.changeDetectorRef.detectChanges(); + } + }) + .catch(() => {}); + }); + + if (!this.syncService.syncInProgress) { + await this.load().catch(() => {}); + } + + this.searchBarService.setEnabled(true); + this.searchBarService.setPlaceholderText(this.i18nService.t("searchVault")); + + const authRequest = await this.apiService.getLastAuthRequest().catch(() => null); + if (authRequest != null) { + this.messagingService.send("openLoginApproval", { + notificationId: authRequest.id, + }); + } + + this.activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(getUserId), + ).catch(() => null); + + if (this.activeUserId) { + this.cipherService + .failedToDecryptCiphers$(this.activeUserId) + .pipe( + map((ciphers) => ciphers?.filter((c) => !c.isDeleted) ?? []), + filter((ciphers) => ciphers.length > 0), + take(1), + takeUntil(this.componentIsDestroyed$), + ) + .subscribe((ciphers) => { + DecryptionFailureDialogComponent.open(this.dialogService, { + cipherIds: ciphers.map((c) => c.id as CipherId), + }); + }); + } + } + + ngOnDestroy() { + this.searchBarService.setEnabled(false); + this.broadcasterService.unsubscribe(BroadcasterSubscriptionId); + this.componentIsDestroyed$.next(true); + this.componentIsDestroyed$.complete(); + } + + async load() { + const params = await firstValueFrom(this.route.queryParams).catch(); + if (params.cipherId) { + const cipherView = new CipherView(); + cipherView.id = params.cipherId; + if (params.action === "clone") { + await this.cloneCipher(cipherView).catch(() => {}); + } else if (params.action === "edit") { + await this.editCipher(cipherView).catch(() => {}); + } else { + await this.viewCipher(cipherView).catch(() => {}); + } + } else if (params.action === "add") { + this.addType = Number(params.addType); + await this.addCipher(this.addType).catch(() => {}); + } + + this.activeFilter = new VaultFilter({ + status: params.deleted ? "trash" : params.favorites ? "favorites" : "all", + cipherType: + params.action === "add" || params.type == null + ? undefined + : (parseInt(params.type) as CipherType), + selectedFolderId: params.folderId, + selectedCollectionId: params.selectedCollectionId, + selectedOrganizationId: params.selectedOrganizationId, + myVaultOnly: params.myVaultOnly ?? false, + }); + if (this.vaultItemsComponent) { + await this.vaultItemsComponent.reload(this.activeFilter.buildFilter()).catch(() => {}); + } + } + + async viewCipher(cipher: CipherView) { + if (await this.shouldReprompt(cipher, "view")) { + return; + } + this.cipherId = cipher.id; + this.cipher = cipher; + this.collections = + this.vaultFilterComponent?.collections.fullList.filter((c) => + cipher.collectionIds.includes(c.id), + ) ?? null; + this.action = "view"; + await this.go().catch(() => {}); + } + + async openAttachmentsDialog() { + if (!this.userHasPremiumAccess) { + await this.premiumUpgradePromptService.promptForPremium(); + return; + } + const dialogRef = AttachmentsV2Component.open(this.dialogService, { + cipherId: this.cipherId as CipherId, + }); + const result = await firstValueFrom(dialogRef.closed).catch(() => null); + if ( + result?.action === AttachmentDialogResult.Removed || + result?.action === AttachmentDialogResult.Uploaded + ) { + await this.vaultItemsComponent?.refresh().catch(() => {}); + } + } + + viewCipherMenu(cipher: CipherView) { + const menu: RendererMenuItem[] = [ + { + label: this.i18nService.t("view"), + click: () => { + this.functionWithChangeDetection(() => { + this.viewCipher(cipher).catch(() => {}); + }); + }, + }, + ]; + + if (cipher.decryptionFailure) { + invokeMenu(menu); + return; + } + + if (!cipher.isDeleted) { + menu.push({ + label: this.i18nService.t("edit"), + click: () => { + this.functionWithChangeDetection(() => { + this.editCipher(cipher).catch(() => {}); + }); + }, + }); + if (!cipher.organizationId) { + menu.push({ + label: this.i18nService.t("clone"), + click: () => { + this.functionWithChangeDetection(() => { + this.cloneCipher(cipher).catch(() => {}); + }); + }, + }); + } + } + + switch (cipher.type) { + case CipherType.Login: + if ( + cipher.login.canLaunch || + cipher.login.username != null || + cipher.login.password != null + ) { + menu.push({ type: "separator" }); + } + if (cipher.login.canLaunch) { + menu.push({ + label: this.i18nService.t("launch"), + click: () => this.platformUtilsService.launchUri(cipher.login.launchUri), + }); + } + if (cipher.login.username != null) { + menu.push({ + label: this.i18nService.t("copyUsername"), + click: () => this.copyValue(cipher, cipher.login.username, "username", "Username"), + }); + } + if (cipher.login.password != null && cipher.viewPassword) { + menu.push({ + label: this.i18nService.t("copyPassword"), + click: () => { + this.copyValue(cipher, cipher.login.password, "password", "Password"); + this.eventCollectionService + .collect(EventType.Cipher_ClientCopiedPassword, cipher.id) + .catch(() => {}); + }, + }); + } + if (cipher.login.hasTotp && (cipher.organizationUseTotp || this.userHasPremiumAccess)) { + menu.push({ + label: this.i18nService.t("copyVerificationCodeTotp"), + click: async () => { + const value = await firstValueFrom( + this.totpService.getCode$(cipher.login.totp), + ).catch(() => null); + if (value) { + this.copyValue(cipher, value.code, "verificationCodeTotp", "TOTP"); + } + }, + }); + } + break; + case CipherType.Card: + if (cipher.card.number != null || cipher.card.code != null) { + menu.push({ type: "separator" }); + } + if (cipher.card.number != null) { + menu.push({ + label: this.i18nService.t("copyNumber"), + click: () => this.copyValue(cipher, cipher.card.number, "number", "Card Number"), + }); + } + if (cipher.card.code != null) { + menu.push({ + label: this.i18nService.t("copySecurityCode"), + click: () => { + this.copyValue(cipher, cipher.card.code, "securityCode", "Security Code"); + this.eventCollectionService + .collect(EventType.Cipher_ClientCopiedCardCode, cipher.id) + .catch(() => {}); + }, + }); + } + break; + default: + break; + } + invokeMenu(menu); + } + + async shouldReprompt(cipher: CipherView, action: "edit" | "clone" | "view"): Promise { + return !(await this.canNavigateAway(action, cipher)) || !(await this.passwordReprompt(cipher)); + } + + async buildFormConfig(action: CipherFormMode) { + this.config = await this.formConfigService + .buildConfig(action, this.cipherId as CipherId, this.addType) + .catch(() => null); + } + + async editCipher(cipher: CipherView) { + if (await this.shouldReprompt(cipher, "edit")) { + return; + } + this.cipherId = cipher.id; + this.cipher = cipher; + await this.buildFormConfig("edit"); + this.action = "edit"; + await this.go().catch(() => {}); + } + + async cloneCipher(cipher: CipherView) { + if (await this.shouldReprompt(cipher, "clone")) { + return; + } + this.cipherId = cipher.id; + this.cipher = cipher; + await this.buildFormConfig("clone"); + this.action = "clone"; + await this.go().catch(() => {}); + } + + async addCipher(type: CipherType) { + this.addType = type || this.activeFilter.cipherType; + this.cipherId = null; + await this.buildFormConfig("add"); + this.action = "add"; + this.prefillCipherFromFilter(); + await this.go().catch(() => {}); + } + + addCipherOptions() { + const menu: RendererMenuItem[] = [ + { + label: this.i18nService.t("typeLogin"), + click: () => this.addCipherWithChangeDetection(CipherType.Login), + }, + { + label: this.i18nService.t("typeCard"), + click: () => this.addCipherWithChangeDetection(CipherType.Card), + }, + { + label: this.i18nService.t("typeIdentity"), + click: () => this.addCipherWithChangeDetection(CipherType.Identity), + }, + { + label: this.i18nService.t("typeSecureNote"), + click: () => this.addCipherWithChangeDetection(CipherType.SecureNote), + }, + ]; + invokeMenu(menu); + } + + async savedCipher(cipher: CipherView) { + this.cipherId = null; + this.action = "view"; + await this.vaultItemsComponent?.refresh().catch(() => {}); + this.cipherId = cipher.id; + this.cipher = cipher; + if (this.activeUserId) { + await this.cipherService.clearCache(this.activeUserId).catch(() => {}); + } + await this.vaultItemsComponent?.load(this.activeFilter.buildFilter()).catch(() => {}); + await this.go().catch(() => {}); + await this.vaultItemsComponent?.refresh().catch(() => {}); + } + + async deleteCipher() { + this.cipherId = null; + this.cipher = null; + this.action = null; + await this.go().catch(() => {}); + await this.vaultItemsComponent?.refresh().catch(() => {}); + } + + async restoreCipher() { + this.cipherId = null; + this.action = null; + await this.go().catch(() => {}); + await this.vaultItemsComponent?.refresh().catch(() => {}); + } + + async cancelCipher(cipher: CipherView) { + this.cipherId = cipher.id; + this.cipher = cipher; + this.action = this.cipherId != null ? "view" : null; + await this.go().catch(() => {}); + } + + async applyVaultFilter(vaultFilter: VaultFilter) { + this.searchBarService.setPlaceholderText( + this.i18nService.t(this.calculateSearchBarLocalizationString(vaultFilter)), + ); + this.activeFilter = vaultFilter; + await this.vaultItemsComponent + ?.reload(this.activeFilter.buildFilter(), vaultFilter.status === "trash") + .catch(() => {}); + await this.go().catch(() => {}); + } + + private calculateSearchBarLocalizationString(vaultFilter: VaultFilter): string { + if (vaultFilter.status === "favorites") { + return "searchFavorites"; + } + if (vaultFilter.status === "trash") { + return "searchTrash"; + } + if (vaultFilter.cipherType != null) { + return "searchType"; + } + if (vaultFilter.selectedFolderId != null && vaultFilter.selectedFolderId !== "none") { + return "searchFolder"; + } + if (vaultFilter.selectedCollectionId != null) { + return "searchCollection"; + } + if (vaultFilter.selectedOrganizationId != null) { + return "searchOrganization"; + } + if (vaultFilter.myVaultOnly) { + return "searchMyVault"; + } + return "searchVault"; + } + + async addFolder() { + this.messagingService.send("newFolder"); + } + + async editFolder(folderId: string) { + if (this.modal != null) { + this.modal.close(); + } + if (this.folderAddEditModalRef == null) { + return; + } + const [modal, childComponent] = await this.modalService + .openViewRef( + FolderAddEditComponent, + this.folderAddEditModalRef, + (comp) => (comp.folderId = folderId), + ) + .catch(() => [null, null] as any); + this.modal = modal; + if (childComponent) { + childComponent.onSavedFolder.subscribe(async (folder: FolderView) => { + this.modal?.close(); + await this.vaultFilterComponent + ?.reloadCollectionsAndFolders(this.activeFilter) + .catch(() => {}); + }); + childComponent.onDeletedFolder.subscribe(async (folder: FolderView) => { + this.modal?.close(); + await this.vaultFilterComponent + ?.reloadCollectionsAndFolders(this.activeFilter) + .catch(() => {}); + }); + } + if (this.modal) { + this.modal.onClosed.pipe(takeUntilDestroyed()).subscribe(() => { + this.modal = null; + }); + } + } + + private dirtyInput(): boolean { + return ( + (this.action === "add" || this.action === "edit" || this.action === "clone") && + document.querySelectorAll("vault-cipher-form .ng-dirty").length > 0 + ); + } + + private async wantsToSaveChanges(): Promise { + const confirmed = await this.dialogService + .openSimpleDialog({ + title: { key: "unsavedChangesTitle" }, + content: { key: "unsavedChangesConfirmation" }, + type: "warning", + }) + .catch(() => false); + return !confirmed; + } + + private async go(queryParams: any = null) { + if (queryParams == null) { + queryParams = { + action: this.action, + cipherId: this.cipherId, + favorites: this.favorites ? true : null, + type: this.type, + folderId: this.folderId, + collectionId: this.collectionId, + deleted: this.deleted ? true : null, + organizationId: this.organizationId, + myVaultOnly: this.myVaultOnly, + }; + } + this.router + .navigate([], { + relativeTo: this.route, + queryParams: queryParams, + replaceUrl: true, + }) + .catch(() => {}); + } + + private addCipherWithChangeDetection(type: CipherType) { + this.functionWithChangeDetection(() => this.addCipher(type).catch(() => {})); + } + + private copyValue(cipher: CipherView, value: string, labelI18nKey: string, aType: string) { + this.functionWithChangeDetection(() => { + (async () => { + if ( + cipher.reprompt !== CipherRepromptType.None && + this.passwordRepromptService.protectedFields().includes(aType) && + !(await this.passwordReprompt(cipher)) + ) { + return; + } + this.platformUtilsService.copyToClipboard(value); + this.toastService.showToast({ + variant: "info", + title: undefined, + message: this.i18nService.t("valueCopied", this.i18nService.t(labelI18nKey)), + }); + if (this.action === "view") { + this.messagingService.send("minimizeOnCopy"); + } + })().catch(() => {}); + }); + } + + private functionWithChangeDetection(func: () => void) { + this.ngZone.run(() => { + func(); + this.changeDetectorRef.detectChanges(); + }); + } + + private prefillCipherFromFilter() { + if (this.activeFilter.selectedCollectionId != null && this.vaultFilterComponent != null) { + const collections = this.vaultFilterComponent.collections.fullList.filter( + (c) => c.id === this.activeFilter.selectedCollectionId, + ); + if (collections.length > 0) { + this.addOrganizationId = collections[0].organizationId; + this.addCollectionIds = [this.activeFilter.selectedCollectionId]; + } + } else if (this.activeFilter.selectedOrganizationId) { + this.addOrganizationId = this.activeFilter.selectedOrganizationId; + } + if (this.activeFilter.selectedFolderId && this.activeFilter.selectedFolder) { + this.folderId = this.activeFilter.selectedFolderId; + } + } + + private async canNavigateAway(action: string, cipher?: CipherView) { + if (this.action === action && (!cipher || this.cipherId === cipher.id)) { + return false; + } else if (this.dirtyInput() && (await this.wantsToSaveChanges())) { + return false; + } + return true; + } + + private async passwordReprompt(cipher: CipherView) { + if (cipher.reprompt === CipherRepromptType.None) { + this.cipherRepromptId = null; + return true; + } + if (this.cipherRepromptId === cipher.id) { + return true; + } + const repromptResult = await this.passwordRepromptService.showPasswordPrompt(); + if (repromptResult) { + this.cipherRepromptId = cipher.id; + } + return repromptResult; + } +} 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 97193bf1b1f..8cb54d9a911 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 @@ -69,6 +69,8 @@ import { ToastService, } from "@bitwarden/components"; import { + AttachmentDialogResult, + AttachmentsV2Component, CipherFormConfig, CipherFormConfigService, CollectionAssignmentResult, @@ -92,10 +94,6 @@ import { } from "../../../vault/components/vault-item-dialog/vault-item-dialog.component"; import { VaultItemEvent } from "../../../vault/components/vault-items/vault-item-event"; import { VaultItemsModule } from "../../../vault/components/vault-items/vault-items.module"; -import { - AttachmentDialogResult, - AttachmentsV2Component, -} from "../../../vault/individual-vault/attachments-v2.component"; import { BulkDeleteDialogResult, openBulkDeleteDialog, diff --git a/apps/web/src/app/auth/settings/emergency-access/view/emergency-view-dialog.component.ts b/apps/web/src/app/auth/settings/emergency-access/view/emergency-view-dialog.component.ts index bcae11c3264..0022da7f3a9 100644 --- a/apps/web/src/app/auth/settings/emergency-access/view/emergency-view-dialog.component.ts +++ b/apps/web/src/app/auth/settings/emergency-access/view/emergency-view-dialog.component.ts @@ -2,6 +2,7 @@ import { CommonModule } from "@angular/common"; import { Component, Inject } from "@angular/core"; import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { VaultViewPasswordHistoryService } from "@bitwarden/angular/services/view-password-history.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { EmergencyAccessId } from "@bitwarden/common/types/guid"; import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service"; @@ -21,8 +22,6 @@ import { DefaultChangeLoginPasswordService, } from "@bitwarden/vault"; -import { WebViewPasswordHistoryService } from "../../../../vault/services/web-view-password-history.service"; - export interface EmergencyViewDialogParams { /** The cipher being viewed. */ cipher: CipherView; @@ -42,7 +41,7 @@ class PremiumUpgradePromptNoop implements PremiumUpgradePromptService { standalone: true, imports: [ButtonModule, CipherViewComponent, DialogModule, CommonModule, JslibModule], providers: [ - { provide: ViewPasswordHistoryService, useClass: WebViewPasswordHistoryService }, + { provide: ViewPasswordHistoryService, useClass: VaultViewPasswordHistoryService }, { provide: PremiumUpgradePromptService, useClass: PremiumUpgradePromptNoop }, { provide: ChangeLoginPasswordService, useClass: DefaultChangeLoginPasswordService }, ], diff --git a/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts b/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts index 10466503029..99159e7e2fc 100644 --- a/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts +++ b/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts @@ -7,6 +7,7 @@ import { firstValueFrom, Subject, switchMap } from "rxjs"; import { map } from "rxjs/operators"; import { CollectionView } from "@bitwarden/admin-console/common"; +import { VaultViewPasswordHistoryService } from "@bitwarden/angular/services/view-password-history.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; @@ -39,6 +40,9 @@ import { ToastService, } from "@bitwarden/components"; import { + AttachmentDialogCloseResult, + AttachmentDialogResult, + AttachmentsV2Component, ChangeLoginPasswordService, CipherFormComponent, CipherFormConfig, @@ -50,16 +54,10 @@ import { } from "@bitwarden/vault"; import { SharedModule } from "../../../shared/shared.module"; -import { - AttachmentDialogCloseResult, - AttachmentDialogResult, - AttachmentsV2Component, -} from "../../individual-vault/attachments-v2.component"; +import { WebVaultPremiumUpgradePromptService } from "../../../vault/services/web-premium-upgrade-prompt.service"; import { RoutedVaultFilterService } from "../../individual-vault/vault-filter/services/routed-vault-filter.service"; import { RoutedVaultFilterModel } from "../../individual-vault/vault-filter/shared/models/routed-vault-filter.model"; import { WebCipherFormGenerationService } from "../../services/web-cipher-form-generation.service"; -import { WebVaultPremiumUpgradePromptService } from "../../services/web-premium-upgrade-prompt.service"; -import { WebViewPasswordHistoryService } from "../../services/web-view-password-history.service"; export type VaultItemDialogMode = "view" | "form"; @@ -135,7 +133,7 @@ export enum VaultItemDialogResult { ], providers: [ { provide: PremiumUpgradePromptService, useClass: WebVaultPremiumUpgradePromptService }, - { provide: ViewPasswordHistoryService, useClass: WebViewPasswordHistoryService }, + { provide: ViewPasswordHistoryService, useClass: VaultViewPasswordHistoryService }, { provide: CipherFormGenerationService, useClass: WebCipherFormGenerationService }, RoutedVaultFilterService, { provide: ChangeLoginPasswordService, useClass: DefaultChangeLoginPasswordService }, diff --git a/apps/web/src/app/vault/individual-vault/add-edit-v2.component.ts b/apps/web/src/app/vault/individual-vault/add-edit-v2.component.ts index d9b42594d79..2eab6faec36 100644 --- a/apps/web/src/app/vault/individual-vault/add-edit-v2.component.ts +++ b/apps/web/src/app/vault/individual-vault/add-edit-v2.component.ts @@ -21,6 +21,7 @@ import { ItemModule, } from "@bitwarden/components"; import { + AttachmentsV2Component, CipherAttachmentsComponent, CipherFormConfig, CipherFormGenerationService, @@ -31,8 +32,6 @@ import { import { SharedModule } from "../../shared/shared.module"; import { WebCipherFormGenerationService } from "../services/web-cipher-form-generation.service"; -import { AttachmentsV2Component } from "./attachments-v2.component"; - /** * The result of the AddEditCipherDialogV2 component. */ 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 7055f164a53..5f56ecc9e04 100644 --- a/apps/web/src/app/vault/individual-vault/vault.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault.component.ts @@ -69,6 +69,9 @@ import { DialogRef, DialogService, Icons, ToastService } from "@bitwarden/compon import { AddEditFolderDialogComponent, AddEditFolderDialogResult, + AttachmentDialogCloseResult, + AttachmentDialogResult, + AttachmentsV2Component, CipherFormConfig, CollectionAssignmentResult, DecryptionFailureDialogComponent, @@ -96,11 +99,6 @@ import { VaultItem } from "../components/vault-items/vault-item"; import { VaultItemEvent } from "../components/vault-items/vault-item-event"; import { VaultItemsModule } from "../components/vault-items/vault-items.module"; -import { - AttachmentDialogCloseResult, - AttachmentDialogResult, - AttachmentsV2Component, -} from "./attachments-v2.component"; import { BulkDeleteDialogResult, openBulkDeleteDialog, diff --git a/apps/web/src/app/vault/individual-vault/view.component.ts b/apps/web/src/app/vault/individual-vault/view.component.ts index 3e5cced7fa8..e7b06cbb8d6 100644 --- a/apps/web/src/app/vault/individual-vault/view.component.ts +++ b/apps/web/src/app/vault/individual-vault/view.component.ts @@ -5,6 +5,7 @@ import { Component, EventEmitter, Inject, OnInit } from "@angular/core"; import { firstValueFrom, map, Observable } from "rxjs"; import { CollectionView } from "@bitwarden/admin-console/common"; +import { VaultViewPasswordHistoryService } from "@bitwarden/angular/services/view-password-history.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; @@ -21,8 +22,8 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service"; import { DIALOG_DATA, - DialogConfig, DialogRef, + DialogConfig, AsyncActionsModule, DialogModule, DialogService, @@ -31,8 +32,7 @@ import { import { CipherViewComponent } from "@bitwarden/vault"; import { SharedModule } from "../../shared/shared.module"; -import { WebVaultPremiumUpgradePromptService } from "../services/web-premium-upgrade-prompt.service"; -import { WebViewPasswordHistoryService } from "../services/web-view-password-history.service"; +import { WebVaultPremiumUpgradePromptService } from "../../vault/services/web-premium-upgrade-prompt.service"; export interface ViewCipherDialogParams { cipher: CipherView; @@ -74,7 +74,7 @@ export interface ViewCipherDialogCloseResult { standalone: true, imports: [CipherViewComponent, CommonModule, AsyncActionsModule, DialogModule, SharedModule], providers: [ - { provide: ViewPasswordHistoryService, useClass: WebViewPasswordHistoryService }, + { provide: ViewPasswordHistoryService, useClass: VaultViewPasswordHistoryService }, { provide: PremiumUpgradePromptService, useClass: WebVaultPremiumUpgradePromptService }, ], }) diff --git a/apps/web/src/app/vault/services/web-view-password-history.service.spec.ts b/libs/angular/src/services/view-password-history.service.spec.ts similarity index 69% rename from apps/web/src/app/vault/services/web-view-password-history.service.spec.ts rename to libs/angular/src/services/view-password-history.service.spec.ts index a4f73ed1a2e..dec2b25b190 100644 --- a/apps/web/src/app/vault/services/web-view-password-history.service.spec.ts +++ b/libs/angular/src/services/view-password-history.service.spec.ts @@ -3,17 +3,16 @@ import { TestBed } from "@angular/core/testing"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { DialogService } from "@bitwarden/components"; +import { openPasswordHistoryDialog } from "@bitwarden/vault"; -import { openPasswordHistoryDialog } from "../individual-vault/password-history.component"; +import { VaultViewPasswordHistoryService } from "./view-password-history.service"; -import { WebViewPasswordHistoryService } from "./web-view-password-history.service"; - -jest.mock("../individual-vault/password-history.component", () => ({ +jest.mock("@bitwarden/vault", () => ({ openPasswordHistoryDialog: jest.fn(), })); -describe("WebViewPasswordHistoryService", () => { - let service: WebViewPasswordHistoryService; +describe("VaultViewPasswordHistoryService", () => { + let service: VaultViewPasswordHistoryService; let dialogService: DialogService; beforeEach(async () => { @@ -23,13 +22,13 @@ describe("WebViewPasswordHistoryService", () => { await TestBed.configureTestingModule({ providers: [ - WebViewPasswordHistoryService, + VaultViewPasswordHistoryService, { provide: DialogService, useValue: mockDialogService }, Overlay, ], }).compileComponents(); - service = TestBed.inject(WebViewPasswordHistoryService); + service = TestBed.inject(VaultViewPasswordHistoryService); dialogService = TestBed.inject(DialogService); }); diff --git a/apps/web/src/app/vault/services/web-view-password-history.service.ts b/libs/angular/src/services/view-password-history.service.ts similarity index 78% rename from apps/web/src/app/vault/services/web-view-password-history.service.ts rename to libs/angular/src/services/view-password-history.service.ts index b1451b268de..88ca4d37287 100644 --- a/apps/web/src/app/vault/services/web-view-password-history.service.ts +++ b/libs/angular/src/services/view-password-history.service.ts @@ -3,14 +3,13 @@ import { Injectable } from "@angular/core"; import { ViewPasswordHistoryService } from "@bitwarden/common/vault/abstractions/view-password-history.service"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { DialogService } from "@bitwarden/components"; - -import { openPasswordHistoryDialog } from "../individual-vault/password-history.component"; +import { openPasswordHistoryDialog } from "@bitwarden/vault"; /** - * This service is used to display the password history dialog in the web vault. + * This service is used to display the password history dialog in the vault. */ @Injectable() -export class WebViewPasswordHistoryService implements ViewPasswordHistoryService { +export class VaultViewPasswordHistoryService implements ViewPasswordHistoryService { constructor(private dialogService: DialogService) {} /** diff --git a/libs/angular/src/vault/components/vault-items.component.ts b/libs/angular/src/vault/components/vault-items.component.ts index 852302cc0c4..c34816994be 100644 --- a/libs/angular/src/vault/components/vault-items.component.ts +++ b/libs/angular/src/vault/components/vault-items.component.ts @@ -18,6 +18,7 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; @Directive() @@ -25,13 +26,14 @@ export class VaultItemsComponent implements OnInit, OnDestroy { @Input() activeCipherId: string = null; @Output() onCipherClicked = new EventEmitter(); @Output() onCipherRightClicked = new EventEmitter(); - @Output() onAddCipher = new EventEmitter(); + @Output() onAddCipher = new EventEmitter(); @Output() onAddCipherOptions = new EventEmitter(); loaded = false; ciphers: CipherView[] = []; deleted = false; organization: Organization; + CipherType = CipherType; protected searchPending = false; @@ -109,8 +111,8 @@ export class VaultItemsComponent implements OnInit, OnDestroy { this.onCipherRightClicked.emit(cipher); } - addCipher() { - this.onAddCipher.emit(); + addCipher(type?: CipherType) { + this.onAddCipher.emit(type); } addCipherOptions() { diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index 33843932382..e353d79988f 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -57,6 +57,7 @@ export enum FeatureFlag { VaultBulkManagementAction = "vault-bulk-management-action", SecurityTasks = "security-tasks", CipherKeyEncryption = "cipher-key-encryption", + PM18520_UpdateDesktopCipherForm = "pm-18520-desktop-cipher-forms", EndUserNotifications = "pm-10609-end-user-notifications", /* Platform */ @@ -110,6 +111,7 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.VaultBulkManagementAction]: FALSE, [FeatureFlag.SecurityTasks]: FALSE, [FeatureFlag.CipherKeyEncryption]: FALSE, + [FeatureFlag.PM18520_UpdateDesktopCipherForm]: FALSE, [FeatureFlag.EndUserNotifications]: FALSE, /* Auth */ diff --git a/libs/vault/src/cipher-form/components/identity/identity.component.ts b/libs/vault/src/cipher-form/components/identity/identity.component.ts index 02bee40c2c7..f0c73002e97 100644 --- a/libs/vault/src/cipher-form/components/identity/identity.component.ts +++ b/libs/vault/src/cipher-form/components/identity/identity.component.ts @@ -132,22 +132,22 @@ export class IdentitySectionComponent implements OnInit { private initFromExistingCipher(existingIdentity: IdentityView) { this.identityForm.patchValue({ - firstName: this.initialValues.firstName ?? existingIdentity.firstName, - middleName: this.initialValues.middleName ?? existingIdentity.middleName, - lastName: this.initialValues.lastName ?? existingIdentity.lastName, - company: this.initialValues.company ?? existingIdentity.company, - ssn: this.initialValues.ssn ?? existingIdentity.ssn, - passportNumber: this.initialValues.passportNumber ?? existingIdentity.passportNumber, - licenseNumber: this.initialValues.licenseNumber ?? existingIdentity.licenseNumber, - email: this.initialValues.email ?? existingIdentity.email, - phone: this.initialValues.phone ?? existingIdentity.phone, - address1: this.initialValues.address1 ?? existingIdentity.address1, - address2: this.initialValues.address2 ?? existingIdentity.address2, - address3: this.initialValues.address3 ?? existingIdentity.address3, - city: this.initialValues.city ?? existingIdentity.city, - state: this.initialValues.state ?? existingIdentity.state, - postalCode: this.initialValues.postalCode ?? existingIdentity.postalCode, - country: this.initialValues.country ?? existingIdentity.country, + firstName: this.initialValues?.firstName ?? existingIdentity.firstName, + middleName: this.initialValues?.middleName ?? existingIdentity.middleName, + lastName: this.initialValues?.lastName ?? existingIdentity.lastName, + company: this.initialValues?.company ?? existingIdentity.company, + ssn: this.initialValues?.ssn ?? existingIdentity.ssn, + passportNumber: this.initialValues?.passportNumber ?? existingIdentity.passportNumber, + licenseNumber: this.initialValues?.licenseNumber ?? existingIdentity.licenseNumber, + email: this.initialValues?.email ?? existingIdentity.email, + phone: this.initialValues?.phone ?? existingIdentity.phone, + address1: this.initialValues?.address1 ?? existingIdentity.address1, + address2: this.initialValues?.address2 ?? existingIdentity.address2, + address3: this.initialValues?.address3 ?? existingIdentity.address3, + city: this.initialValues?.city ?? existingIdentity.city, + state: this.initialValues?.state ?? existingIdentity.state, + postalCode: this.initialValues?.postalCode ?? existingIdentity.postalCode, + country: this.initialValues?.country ?? existingIdentity.country, }); } diff --git a/apps/web/src/app/vault/individual-vault/attachments-v2.component.html b/libs/vault/src/cipher-view/attachments/attachments-v2.component.html similarity index 100% rename from apps/web/src/app/vault/individual-vault/attachments-v2.component.html rename to libs/vault/src/cipher-view/attachments/attachments-v2.component.html diff --git a/apps/web/src/app/vault/individual-vault/attachments-v2.component.spec.ts b/libs/vault/src/cipher-view/attachments/attachments-v2.component.spec.ts similarity index 100% rename from apps/web/src/app/vault/individual-vault/attachments-v2.component.spec.ts rename to libs/vault/src/cipher-view/attachments/attachments-v2.component.spec.ts diff --git a/apps/web/src/app/vault/individual-vault/attachments-v2.component.ts b/libs/vault/src/cipher-view/attachments/attachments-v2.component.ts similarity index 84% rename from apps/web/src/app/vault/individual-vault/attachments-v2.component.ts rename to libs/vault/src/cipher-view/attachments/attachments-v2.component.ts index 949a2fa81d9..fd823353099 100644 --- a/apps/web/src/app/vault/individual-vault/attachments-v2.component.ts +++ b/libs/vault/src/cipher-view/attachments/attachments-v2.component.ts @@ -4,10 +4,16 @@ import { CommonModule } from "@angular/common"; import { Component, Inject } from "@angular/core"; import { CipherId } from "@bitwarden/common/types/guid"; -import { DialogRef, DIALOG_DATA, DialogService } from "@bitwarden/components"; -import { CipherAttachmentsComponent } from "@bitwarden/vault"; +import { + ButtonModule, + DialogModule, + DialogService, + DIALOG_DATA, + DialogRef, +} from "@bitwarden/components"; +import { I18nPipe } from "@bitwarden/ui-common"; -import { SharedModule } from "../../shared/shared.module"; +import { CipherAttachmentsComponent } from "../../cipher-form/components/attachments/cipher-attachments.component"; export interface AttachmentsDialogParams { cipherId: CipherId; @@ -33,7 +39,7 @@ export interface AttachmentDialogCloseResult { selector: "app-vault-attachments-v2", templateUrl: "attachments-v2.component.html", standalone: true, - imports: [CommonModule, SharedModule, CipherAttachmentsComponent], + imports: [ButtonModule, CommonModule, DialogModule, I18nPipe, CipherAttachmentsComponent], }) export class AttachmentsV2Component { cipherId: CipherId; diff --git a/libs/vault/src/cipher-view/cipher-view.component.ts b/libs/vault/src/cipher-view/cipher-view.component.ts index 48b70271e43..e748fa46656 100644 --- a/libs/vault/src/cipher-view/cipher-view.component.ts +++ b/libs/vault/src/cipher-view/cipher-view.component.ts @@ -124,7 +124,8 @@ export class CipherViewComponent implements OnChanges, OnDestroy { } const { username, password, totp, fido2Credentials } = this.cipher.login; - return username || password || totp || fido2Credentials; + + return username || password || totp || fido2Credentials?.length > 0; } get hasAutofill() { diff --git a/libs/vault/src/cipher-view/index.ts b/libs/vault/src/cipher-view/index.ts index ec3c5235077..9bcdaaa4385 100644 --- a/libs/vault/src/cipher-view/index.ts +++ b/libs/vault/src/cipher-view/index.ts @@ -1,2 +1,3 @@ export * from "./cipher-view.component"; +export * from "./attachments/attachments-v2.component"; export { CipherAttachmentsComponent } from "../cipher-form/components/attachments/cipher-attachments.component"; diff --git a/apps/web/src/app/vault/individual-vault/password-history.component.html b/libs/vault/src/components/password-history/password-history.component.html similarity index 100% rename from apps/web/src/app/vault/individual-vault/password-history.component.html rename to libs/vault/src/components/password-history/password-history.component.html diff --git a/apps/web/src/app/vault/individual-vault/password-history.component.ts b/libs/vault/src/components/password-history/password-history.component.ts similarity index 95% rename from apps/web/src/app/vault/individual-vault/password-history.component.ts rename to libs/vault/src/components/password-history/password-history.component.ts index 5207e1b9e40..5af785d1a70 100644 --- a/apps/web/src/app/vault/individual-vault/password-history.component.ts +++ b/libs/vault/src/components/password-history/password-history.component.ts @@ -5,17 +5,17 @@ import { Inject, Component } from "@angular/core"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { - DIALOG_DATA, - DialogConfig, - DialogRef, AsyncActionsModule, + ButtonModule, DialogModule, DialogService, + DIALOG_DATA, + DialogRef, + DialogConfig, } from "@bitwarden/components"; +import { I18nPipe } from "@bitwarden/ui-common"; import { PasswordHistoryViewComponent } from "@bitwarden/vault"; -import { SharedModule } from "../../shared/shared.module"; - /** * The parameters for the password history dialog. */ @@ -31,10 +31,11 @@ export interface ViewPasswordHistoryDialogParams { templateUrl: "password-history.component.html", standalone: true, imports: [ + ButtonModule, CommonModule, AsyncActionsModule, + I18nPipe, DialogModule, - SharedModule, PasswordHistoryViewComponent, ], }) diff --git a/libs/vault/src/index.ts b/libs/vault/src/index.ts index 655b536de64..6e5a452ec8c 100644 --- a/libs/vault/src/index.ts +++ b/libs/vault/src/index.ts @@ -19,6 +19,7 @@ export { PasswordHistoryViewComponent } from "./components/password-history-view export { NewDeviceVerificationNoticePageOneComponent } from "./components/new-device-verification-notice/new-device-verification-notice-page-one.component"; export { NewDeviceVerificationNoticePageTwoComponent } from "./components/new-device-verification-notice/new-device-verification-notice-page-two.component"; export { DecryptionFailureDialogComponent } from "./components/decryption-failure-dialog/decryption-failure-dialog.component"; +export { openPasswordHistoryDialog } from "./components/password-history/password-history.component"; export * from "./components/add-edit-folder-dialog/add-edit-folder-dialog.component"; export * from "./components/carousel"; From 320d4f65fa6ce0fd3bb9cdc526e6c39ddb66f13a Mon Sep 17 00:00:00 2001 From: Daniel Riera Date: Wed, 23 Apr 2025 14:47:09 -0400 Subject: [PATCH 042/499] PM-20396 open view item vault pop out (#14342) * PM-20396 open view item vault pop out * add aria and clean up * format json * clean naming in messages * revert feature-flag.enum.ts * change username to item name * return nullish operator removed in testing * update tests to account for itemName * revert to anchor tag --- apps/browser/src/_locales/en/messages.json | 43 +++++++++++-------- .../abstractions/notification.background.ts | 4 ++ .../notification.background.spec.ts | 11 +++-- .../background/notification.background.ts | 28 ++++++++++-- .../notification/confirmation/body.ts | 5 ++- .../notification/confirmation/container.ts | 19 ++++---- .../notification/confirmation/message.ts | 17 +++++++- .../abstractions/notification-bar.ts | 2 +- apps/browser/src/autofill/notification/bar.ts | 16 +++---- 9 files changed, 99 insertions(+), 46 deletions(-) diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 462af12a352..d1d05a4e852 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -192,7 +192,7 @@ "message": "Copy", "description": "Copy to clipboard" }, - "fill":{ + "fill": { "message": "Fill", "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." }, @@ -1062,6 +1062,15 @@ "notificationAddSave": { "message": "Save" }, + "notificationViewAria": { + "message": "View $ITEMNAME$, opens in new window", + "placeholders": { + "itemName": { + "content": "$1" + } + }, + "description": "Aria label for the view button in notification bar confirmation message" + }, "newNotification": { "message": "New notification" }, @@ -1075,23 +1084,23 @@ } } }, - "loginSaveSuccessDetails": { - "message": "$USERNAME$ saved to Bitwarden.", - "placeholders": { - "username": { - "content": "$1" - } - }, - "description": "Shown to user after login is saved." + "loginSaveConfirmation": { + "message": "$ITEMNAME$ saved to Bitwarden.", + "placeholders": { + "itemName": { + "content": "$1" + } + }, + "description": "Shown to user after item is saved." }, - "loginUpdatedSuccessDetails": { - "message": "$USERNAME$ updated in Bitwarden.", - "placeholders": { - "username": { - "content": "$1" - } - }, - "description": "Shown to user after login is updated." + "loginUpdatedConfirmation": { + "message": "$ITEMNAME$ updated in Bitwarden.", + "placeholders": { + "itemName": { + "content": "$1" + } + }, + "description": "Shown to user after item is updated." }, "saveAsNewLoginAction": { "message": "Save as new login", diff --git a/apps/browser/src/autofill/background/abstractions/notification.background.ts b/apps/browser/src/autofill/background/abstractions/notification.background.ts index 6b3c91a109c..c93fd9a3acf 100644 --- a/apps/browser/src/autofill/background/abstractions/notification.background.ts +++ b/apps/browser/src/autofill/background/abstractions/notification.background.ts @@ -101,6 +101,10 @@ type NotificationBackgroundExtensionMessageHandlers = { bgChangedPassword: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise; bgRemoveTabFromNotificationQueue: ({ sender }: BackgroundSenderParam) => void; bgSaveCipher: ({ message, sender }: BackgroundOnMessageHandlerParams) => void; + bgOpenViewVaultItemPopout: ({ + message, + sender, + }: BackgroundOnMessageHandlerParams) => Promise; bgOpenVault: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise; bgNeverSave: ({ sender }: BackgroundSenderParam) => Promise; bgUnlockPopoutOpened: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise; diff --git a/apps/browser/src/autofill/background/notification.background.spec.ts b/apps/browser/src/autofill/background/notification.background.spec.ts index ffc416ab62a..bb993fcf94b 100644 --- a/apps/browser/src/autofill/background/notification.background.spec.ts +++ b/apps/browser/src/autofill/background/notification.background.spec.ts @@ -823,6 +823,7 @@ describe("NotificationBackground", () => { notificationBackground["notificationQueue"] = [queueMessage]; const cipherView = mock({ id: "testId", + name: "testItemName", login: { username: "testUser" }, }); getDecryptedCipherByIdSpy.mockResolvedValueOnce(cipherView); @@ -844,8 +845,9 @@ describe("NotificationBackground", () => { sender.tab, "saveCipherAttemptCompleted", { - username: cipherView.login.username, + itemName: "testItemName", cipherId: cipherView.id, + task: undefined, }, ); }); @@ -899,7 +901,7 @@ describe("NotificationBackground", () => { const cipherView = mock({ id: mockCipherId, organizationId: mockOrgId, - login: { username: "testUser" }, + name: "Test Item", }); getDecryptedCipherByIdSpy.mockResolvedValueOnce(cipherView); @@ -921,11 +923,11 @@ describe("NotificationBackground", () => { "saveCipherAttemptCompleted", { cipherId: "testId", + itemName: "Test Item", task: { orgName: "Org Name, LLC", remainingTasksCount: 1, }, - username: "testUser", }, ); }); @@ -1074,6 +1076,7 @@ describe("NotificationBackground", () => { notificationBackground["notificationQueue"] = [queueMessage]; const cipherView = mock({ id: "testId", + name: "testName", login: { username: "test", password: "password" }, }); folderExistsSpy.mockResolvedValueOnce(false); @@ -1097,8 +1100,8 @@ describe("NotificationBackground", () => { sender.tab, "saveCipherAttemptCompleted", { - username: cipherView.login.username, cipherId: cipherView.id, + itemName: cipherView.name, }, ); expect(tabSendMessageSpy).toHaveBeenCalledWith(sender.tab, { command: "addedCipher" }); diff --git a/apps/browser/src/autofill/background/notification.background.ts b/apps/browser/src/autofill/background/notification.background.ts index 00d24300d78..dabb75b97b6 100644 --- a/apps/browser/src/autofill/background/notification.background.ts +++ b/apps/browser/src/autofill/background/notification.background.ts @@ -41,7 +41,10 @@ import { SecurityTask } from "@bitwarden/common/vault/tasks/models/security-task import { openUnlockPopout } from "../../auth/popup/utils/auth-popout-window"; import { BrowserApi } from "../../platform/browser/browser-api"; -import { openAddEditVaultItemPopout } from "../../vault/popup/utils/vault-popout-window"; +import { + openAddEditVaultItemPopout, + openViewVaultItemPopout, +} from "../../vault/popup/utils/vault-popout-window"; import { OrganizationCategory, OrganizationCategories, @@ -67,6 +70,7 @@ import { OverlayBackgroundExtensionMessage } from "./abstractions/overlay.backgr export default class NotificationBackground { private openUnlockPopout = openUnlockPopout; private openAddEditVaultItemPopout = openAddEditVaultItemPopout; + private openViewVaultItemPopout = openViewVaultItemPopout; private notificationQueue: NotificationQueueMessageItem[] = []; private allowedRetryCommands: Set = new Set([ ExtensionCommand.AutofillLogin, @@ -91,6 +95,7 @@ export default class NotificationBackground { bgGetOrgData: () => this.getOrgData(), bgNeverSave: ({ sender }) => this.saveNever(sender.tab), bgOpenVault: ({ message, sender }) => this.openVault(message, sender.tab), + bgOpenViewVaultItemPopout: ({ message, sender }) => this.viewItem(message, sender.tab), bgRemoveTabFromNotificationQueue: ({ sender }) => this.removeTabFromNotificationQueue(sender.tab), bgReopenUnlockPopout: ({ sender }) => this.openUnlockPopout(sender.tab), @@ -638,8 +643,8 @@ export default class NotificationBackground { try { await this.cipherService.createWithServer(cipher); await BrowserApi.tabSendMessageData(tab, "saveCipherAttemptCompleted", { - username: queueMessage?.username && String(queueMessage.username), - cipherId: cipher?.id && String(cipher.id), + itemName: newCipher?.name && String(newCipher?.name), + cipherId: cipher?.id && String(cipher?.id), }); await BrowserApi.tabSendMessage(tab, { command: "addedCipher" }); } catch (error) { @@ -701,7 +706,7 @@ export default class NotificationBackground { await this.cipherService.updateWithServer(cipher); await BrowserApi.tabSendMessageData(tab, "saveCipherAttemptCompleted", { - username: cipherView?.login?.username && String(cipherView.login.username), + itemName: cipherView?.name && String(cipherView?.name), cipherId: cipherView?.id && String(cipherView.id), task: taskData, }); @@ -754,6 +759,21 @@ export default class NotificationBackground { await this.openAddEditVaultItemPopout(senderTab, { cipherId: message.cipherId }); } + private async viewItem( + message: NotificationBackgroundExtensionMessage, + senderTab: chrome.tabs.Tab, + ) { + await Promise.all([ + this.openViewVaultItemPopout(senderTab, { + cipherId: message.cipherId, + action: null, + }), + BrowserApi.tabSendMessageData(senderTab, "closeNotificationBar", { + fadeOutNotification: !!message.fadeOutNotification, + }), + ]); + } + private async folderExists(folderId: string, userId: UserId) { if (Utils.isNullOrWhitespace(folderId) || folderId === "null") { return false; diff --git a/apps/browser/src/autofill/content/components/notification/confirmation/body.ts b/apps/browser/src/autofill/content/components/notification/confirmation/body.ts index d2ac7f36277..0508991c5da 100644 --- a/apps/browser/src/autofill/content/components/notification/confirmation/body.ts +++ b/apps/browser/src/autofill/content/components/notification/confirmation/body.ts @@ -16,16 +16,18 @@ const { css } = createEmotion({ export type NotificationConfirmationBodyProps = { buttonText: string; + itemName: string; confirmationMessage: string; error?: string; messageDetails?: string; tasksAreComplete?: boolean; theme: Theme; - handleOpenVault: (e: Event) => void; + handleOpenVault: () => void; }; export function NotificationConfirmationBody({ buttonText, + itemName, confirmationMessage, error, messageDetails, @@ -43,6 +45,7 @@ export function NotificationConfirmationBody({ ${showConfirmationMessage ? NotificationConfirmationMessage({ buttonText, + itemName, message: confirmationMessage, messageDetails, theme, diff --git a/apps/browser/src/autofill/content/components/notification/confirmation/container.ts b/apps/browser/src/autofill/content/components/notification/confirmation/container.ts index a071338af9a..5cc977cf4cb 100644 --- a/apps/browser/src/autofill/content/components/notification/confirmation/container.ts +++ b/apps/browser/src/autofill/content/components/notification/confirmation/container.ts @@ -20,14 +20,14 @@ import { NotificationConfirmationFooter } from "./footer"; export type NotificationConfirmationContainerProps = NotificationBarIframeInitData & { handleCloseNotification: (e: Event) => void; - handleOpenVault: (e: Event) => void; + handleOpenVault: () => void; handleOpenTasks: (e: Event) => void; } & { error?: string; i18n: { [key: string]: string }; + itemName: string; task?: NotificationTaskInfo; type: NotificationType; - username: string; }; export function NotificationConfirmationContainer({ @@ -36,13 +36,13 @@ export function NotificationConfirmationContainer({ handleOpenVault, handleOpenTasks, i18n, + itemName, task, theme = ThemeTypes.Light, type, - username, }: NotificationConfirmationContainerProps) { const headerMessage = getHeaderMessage(i18n, type, error); - const confirmationMessage = getConfirmationMessage(i18n, username, type, error); + const confirmationMessage = getConfirmationMessage(i18n, itemName, type, error); const buttonText = error ? i18n.newItem : i18n.view; let messageDetails: string | undefined; @@ -71,6 +71,7 @@ export function NotificationConfirmationContainer({ })} ${NotificationConfirmationBody({ buttonText, + itemName, confirmationMessage, tasksAreComplete, messageDetails, @@ -106,19 +107,17 @@ const notificationContainerStyles = (theme: Theme) => css` function getConfirmationMessage( i18n: { [key: string]: string }, - username: string, + itemName: string, type?: NotificationType, error?: string, ) { - const loginSaveSuccessDetails = chrome.i18n.getMessage("loginSaveSuccessDetails", [username]); - const loginUpdatedSuccessDetails = chrome.i18n.getMessage("loginUpdatedSuccessDetails", [ - username, - ]); + const loginSaveConfirmation = chrome.i18n.getMessage("loginSaveConfirmation", [itemName]); + const loginUpdatedConfirmation = chrome.i18n.getMessage("loginUpdatedConfirmation", [itemName]); if (error) { return i18n.saveFailureDetails; } - return type === "add" ? loginSaveSuccessDetails : loginUpdatedSuccessDetails; + return type === "add" ? loginSaveConfirmation : loginUpdatedConfirmation; } function getHeaderMessage( diff --git a/apps/browser/src/autofill/content/components/notification/confirmation/message.ts b/apps/browser/src/autofill/content/components/notification/confirmation/message.ts index c018371caff..3707e628370 100644 --- a/apps/browser/src/autofill/content/components/notification/confirmation/message.ts +++ b/apps/browser/src/autofill/content/components/notification/confirmation/message.ts @@ -7,19 +7,23 @@ import { themes, typography } from "../../constants/styles"; export type NotificationConfirmationMessageProps = { buttonText?: string; + itemName: string; message?: string; messageDetails?: string; - handleClick: (e: Event) => void; + handleClick: () => void; theme: Theme; }; export function NotificationConfirmationMessage({ buttonText, + itemName, message, messageDetails, handleClick, theme, }: NotificationConfirmationMessageProps) { + const buttonAria = chrome.i18n.getMessage("notificationViewAria", [itemName]); + return html`
${message || buttonText @@ -35,6 +39,10 @@ export function NotificationConfirmationMessage({ title=${buttonText} class=${notificationConfirmationButtonTextStyles(theme)} @click=${handleClick} + @keydown=${(e: KeyboardEvent) => handleButtonKeyDown(e, handleClick)} + aria-label=${buttonAria} + tabindex="0" + role="button" > ${buttonText} @@ -81,3 +89,10 @@ const AdditionalMessageStyles = ({ theme }: { theme: Theme }) => css` font-size: 14px; color: ${themes[theme].text.muted}; `; + +function handleButtonKeyDown(event: KeyboardEvent, handleClick: () => void) { + if (event.key === "Enter" || event.key === " ") { + event.preventDefault(); + handleClick(); + } +} diff --git a/apps/browser/src/autofill/notification/abstractions/notification-bar.ts b/apps/browser/src/autofill/notification/abstractions/notification-bar.ts index cbfeffcf2f4..9dd02b64154 100644 --- a/apps/browser/src/autofill/notification/abstractions/notification-bar.ts +++ b/apps/browser/src/autofill/notification/abstractions/notification-bar.ts @@ -33,7 +33,7 @@ type NotificationBarWindowMessage = { data?: { cipherId?: string; task?: NotificationTaskInfo; - username?: string; + itemName?: string; }; error?: string; initData?: NotificationBarIframeInitData; diff --git a/apps/browser/src/autofill/notification/bar.ts b/apps/browser/src/autofill/notification/bar.ts index 4e85d893178..fce05913e5e 100644 --- a/apps/browser/src/autofill/notification/bar.ts +++ b/apps/browser/src/autofill/notification/bar.ts @@ -56,9 +56,9 @@ function getI18n() { collection: chrome.i18n.getMessage("collection"), folder: chrome.i18n.getMessage("folder"), loginSaveSuccess: chrome.i18n.getMessage("loginSaveSuccess"), - loginSaveSuccessDetails: chrome.i18n.getMessage("loginSaveSuccessDetails"), + loginSaveConfirmation: chrome.i18n.getMessage("loginSaveConfirmation"), loginUpdateSuccess: chrome.i18n.getMessage("loginUpdateSuccess"), - loginUpdateSuccessDetails: chrome.i18n.getMessage("loginUpdatedSuccessDetails"), + loginUpdateConfirmation: chrome.i18n.getMessage("loginUpdatedConfirmation"), loginUpdateTaskSuccess: chrome.i18n.getMessage("loginUpdateTaskSuccess"), loginUpdateTaskSuccessAdditional: chrome.i18n.getMessage("loginUpdateTaskSuccessAdditional"), nextSecurityTaskAction: chrome.i18n.getMessage("nextSecurityTaskAction"), @@ -72,6 +72,7 @@ function getI18n() { notificationEdit: chrome.i18n.getMessage("edit"), notificationUnlock: chrome.i18n.getMessage("notificationUnlock"), notificationUnlockDesc: chrome.i18n.getMessage("notificationUnlockDesc"), + notificationViewAria: chrome.i18n.getMessage("notificationViewAria"), saveAction: chrome.i18n.getMessage("notificationAddSave"), saveAsNewLoginAction: chrome.i18n.getMessage("saveAsNewLoginAction"), saveFailure: chrome.i18n.getMessage("saveFailure"), @@ -349,10 +350,9 @@ function handleSaveCipherAttemptCompletedMessage(message: NotificationBarWindowM ); } -function openViewVaultItemPopout(e: Event, cipherId: string) { - e.preventDefault(); +function openViewVaultItemPopout(cipherId: string) { sendPlatformMessage({ - command: "bgOpenVault", + command: "bgOpenViewVaultItemPopout", cipherId, }); } @@ -360,7 +360,7 @@ function openViewVaultItemPopout(e: Event, cipherId: string) { function handleSaveCipherConfirmation(message: NotificationBarWindowMessage) { const { theme, type } = notificationBarIframeInitData; const { error, data } = message; - const { username, cipherId, task } = data || {}; + const { cipherId, task, itemName } = data || {}; const i18n = getI18n(); const resolvedTheme = getResolvedTheme(theme ?? ThemeTypes.Light); @@ -374,9 +374,9 @@ function handleSaveCipherConfirmation(message: NotificationBarWindowMessage) { handleCloseNotification, i18n, error, - username: username ?? i18n.typeLogin, + itemName: itemName ?? i18n.typeLogin, task, - handleOpenVault: (e) => cipherId && openViewVaultItemPopout(e, cipherId), + handleOpenVault: () => cipherId && openViewVaultItemPopout(cipherId), handleOpenTasks: () => sendPlatformMessage({ command: "bgOpenAtRisksPasswords" }), }), document.body, From fe3e6fd1985c0b39a3110300713f41267e2b262c Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Wed, 23 Apr 2025 15:26:53 -0700 Subject: [PATCH 043/499] migrate to tw class name (#14317) --- .../components/autofill-options/uri-option.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/vault/src/cipher-form/components/autofill-options/uri-option.component.html b/libs/vault/src/cipher-form/components/autofill-options/uri-option.component.html index 5301e4f32b9..ec98595e93b 100644 --- a/libs/vault/src/cipher-form/components/autofill-options/uri-option.component.html +++ b/libs/vault/src/cipher-form/components/autofill-options/uri-option.component.html @@ -1,5 +1,5 @@ -
+
{{ uriLabel }} From f521afa3ae6fc308866ae95912a47b33d23c7bae Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 24 Apr 2025 14:39:31 +0200 Subject: [PATCH 044/499] [deps] Tools: Update jsdom to v26.1.0 (#14397) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> --- apps/cli/package.json | 2 +- package-lock.json | 23 +++++++++++------------ package.json | 2 +- 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/apps/cli/package.json b/apps/cli/package.json index 1bf6a1d41a1..daec6593543 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -74,7 +74,7 @@ "form-data": "4.0.1", "https-proxy-agent": "7.0.6", "inquirer": "8.2.6", - "jsdom": "26.0.0", + "jsdom": "26.1.0", "jszip": "3.10.1", "koa": "2.16.1", "koa-bodyparser": "4.4.1", diff --git a/package-lock.json b/package-lock.json index 3e16fd7ba68..4e08a6df9a8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -46,7 +46,7 @@ "https-proxy-agent": "7.0.6", "inquirer": "8.2.6", "jquery": "3.7.1", - "jsdom": "26.0.0", + "jsdom": "26.1.0", "jszip": "3.10.1", "koa": "2.16.1", "koa-bodyparser": "4.4.1", @@ -208,7 +208,7 @@ "form-data": "4.0.1", "https-proxy-agent": "7.0.6", "inquirer": "8.2.6", - "jsdom": "26.0.0", + "jsdom": "26.1.0", "jszip": "3.10.1", "koa": "2.16.1", "koa-bodyparser": "4.4.1", @@ -24463,15 +24463,14 @@ } }, "node_modules/jsdom": { - "version": "26.0.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.0.0.tgz", - "integrity": "sha512-BZYDGVAIriBWTpIxYzrXjv3E/4u8+/pSG5bQdIYCbNCGOvsPkDQfTVLAIXAf9ETdCpduCVTkDe2NNZ8NIwUVzw==", + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", + "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", "license": "MIT", "dependencies": { "cssstyle": "^4.2.1", "data-urls": "^5.0.0", - "decimal.js": "^10.4.3", - "form-data": "^4.0.1", + "decimal.js": "^10.5.0", "html-encoding-sniffer": "^4.0.0", "http-proxy-agent": "^7.0.2", "https-proxy-agent": "^7.0.6", @@ -24481,12 +24480,12 @@ "rrweb-cssom": "^0.8.0", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", - "tough-cookie": "^5.0.0", + "tough-cookie": "^5.1.1", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^7.0.0", "whatwg-encoding": "^3.1.1", "whatwg-mimetype": "^4.0.0", - "whatwg-url": "^14.1.0", + "whatwg-url": "^14.1.1", "ws": "^8.18.0", "xml-name-validator": "^5.0.0" }, @@ -24525,9 +24524,9 @@ } }, "node_modules/jsdom/node_modules/tough-cookie": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.1.tgz", - "integrity": "sha512-Ek7HndSVkp10hmHP9V4qZO1u+pn1RU5sI0Fw+jCU3lyvuMZcgqsNgc6CmJJZyByK4Vm/qotGRJlfgAX8q+4JiA==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", "license": "BSD-3-Clause", "dependencies": { "tldts": "^6.1.32" diff --git a/package.json b/package.json index c78decb9827..861559b2b66 100644 --- a/package.json +++ b/package.json @@ -178,7 +178,7 @@ "https-proxy-agent": "7.0.6", "inquirer": "8.2.6", "jquery": "3.7.1", - "jsdom": "26.0.0", + "jsdom": "26.1.0", "jszip": "3.10.1", "koa": "2.16.1", "koa-bodyparser": "4.4.1", From 1e6a605ead4517a636cc66eabf9f43b0fecbf2ef Mon Sep 17 00:00:00 2001 From: Daniel Riera Date: Thu, 24 Apr 2025 10:51:21 -0400 Subject: [PATCH 045/499] PM-20393 return only matching ciphers on type of change (#14392) * PM-20393 return only matching ciphers on type of change * use type for change --- .../background/notification.background.ts | 53 ++++++++++--------- 1 file changed, 27 insertions(+), 26 deletions(-) diff --git a/apps/browser/src/autofill/background/notification.background.ts b/apps/browser/src/autofill/background/notification.background.ts index dabb75b97b6..0b9c6244987 100644 --- a/apps/browser/src/autofill/background/notification.background.ts +++ b/apps/browser/src/autofill/background/notification.background.ts @@ -160,51 +160,42 @@ export default class NotificationBackground { /** * - * Gets the current active tab and retrieves all decrypted ciphers - * for the tab's URL. It constructs and returns an array of `NotificationCipherData` objects. + * Gets the current active tab and retrieves the relevant decrypted cipher + * for the tab's URL. It constructs and returns an array of `NotificationCipherData` objects or a singular object. * If no active tab or URL is found, it returns an empty array. * * @returns {Promise} */ async getNotificationCipherData(): Promise { - const [currentTab, showFavicons, env] = await Promise.all([ + const [currentTab, showFavicons, env, activeUserId] = await Promise.all([ BrowserApi.getTabFromCurrentWindow(), firstValueFrom(this.domainSettingsService.showFavicons$), firstValueFrom(this.environmentService.environment$), + firstValueFrom(this.accountService.activeAccount$.pipe(getOptionalUserId)), + ]); + + const [decryptedCiphers, organizations] = await Promise.all([ + this.cipherService.getAllDecryptedForUrl(currentTab?.url, activeUserId), + firstValueFrom(this.organizationService.organizations$(activeUserId)), ]); const iconsServerUrl = env.getIconsUrl(); - const activeUserId = await firstValueFrom( - this.accountService.activeAccount$.pipe(getOptionalUserId), - ); - const decryptedCiphers = await this.cipherService.getAllDecryptedForUrl( - currentTab?.url, - activeUserId, - ); - - const organizations = await firstValueFrom( - this.organizationService.organizations$(activeUserId), - ); - - return decryptedCiphers.map((view) => { + const toNotificationData = (view: CipherView): NotificationCipherData => { const { id, name, reprompt, favorite, login, organizationId } = view; - const organizationType = organizationId - ? organizations.find((org) => org.id === organizationId)?.productTierType - : null; + const type = organizations.find((org) => org.id === organizationId)?.productTierType; const organizationCategories: OrganizationCategory[] = []; - if ( [ProductTierType.Teams, ProductTierType.Enterprise, ProductTierType.TeamsStarter].includes( - organizationType, + type, ) ) { organizationCategories.push(OrganizationCategories.business); } - if ([ProductTierType.Families, ProductTierType.Free].includes(organizationType)) { + if ([ProductTierType.Families, ProductTierType.Free].includes(type)) { organizationCategories.push(OrganizationCategories.family); } @@ -216,11 +207,21 @@ export default class NotificationBackground { favorite, ...(organizationCategories.length ? { organizationCategories } : {}), icon: buildCipherIcon(iconsServerUrl, view, showFavicons), - login: login && { - username: login.username, - }, + login: login && { username: login.username }, }; - }); + }; + + const changeItem = this.notificationQueue.find( + (message): message is AddChangePasswordQueueMessage => + message.type === NotificationQueueMessageType.ChangePassword, + ); + + if (changeItem) { + const cipherView = await this.getDecryptedCipherById(changeItem.cipherId, activeUserId); + return [toNotificationData(cipherView)]; + } + + return decryptedCiphers.map(toNotificationData); } /** From 26273ca07201c68bdbc182eb32ad593af5cbefc1 Mon Sep 17 00:00:00 2001 From: Todd Martin <106564991+trmartin4@users.noreply.github.com> Date: Thu, 24 Apr 2025 13:46:20 -0400 Subject: [PATCH 046/499] Update copyright year to 2025 (#14364) Co-authored-by: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> --- apps/browser/src/safari/desktop/Info.plist | 2 +- apps/browser/src/safari/safari/Info.plist | 2 +- apps/cli/stores/chocolatey/bitwarden-cli.nuspec | 2 +- apps/desktop/electron-builder.json | 2 +- apps/desktop/stores/chocolatey/bitwarden.nuspec | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/browser/src/safari/desktop/Info.plist b/apps/browser/src/safari/desktop/Info.plist index 69ea518a0ae..b687d9d2f3a 100644 --- a/apps/browser/src/safari/desktop/Info.plist +++ b/apps/browser/src/safari/desktop/Info.plist @@ -25,7 +25,7 @@ LSMinimumSystemVersion $(MACOSX_DEPLOYMENT_TARGET) NSHumanReadableCopyright - Copyright © 2015-2024 Bitwarden Inc. All rights reserved. + Copyright © 2015-2025 Bitwarden Inc. All rights reserved. NSMainStoryboardFile Main NSPrincipalClass diff --git a/apps/browser/src/safari/safari/Info.plist b/apps/browser/src/safari/safari/Info.plist index b79ed132ea9..95172846758 100644 --- a/apps/browser/src/safari/safari/Info.plist +++ b/apps/browser/src/safari/safari/Info.plist @@ -30,7 +30,7 @@ $(PRODUCT_MODULE_NAME).SafariWebExtensionHandler NSHumanReadableCopyright - Copyright © 2015-2024 Bitwarden Inc. All rights reserved. + Copyright © 2015-2025 Bitwarden Inc. All rights reserved. NSHumanReadableDescription A secure and free password manager for all of your devices. SFSafariAppExtensionBundleIdentifiersToReplace diff --git a/apps/cli/stores/chocolatey/bitwarden-cli.nuspec b/apps/cli/stores/chocolatey/bitwarden-cli.nuspec index e5ce03fa49d..f7f86bc843f 100644 --- a/apps/cli/stores/chocolatey/bitwarden-cli.nuspec +++ b/apps/cli/stores/chocolatey/bitwarden-cli.nuspec @@ -10,7 +10,7 @@ Bitwarden Inc. https://bitwarden.com/ https://raw.githubusercontent.com/bitwarden/brand/master/icons/256x256.png - Copyright © 2015-2024 Bitwarden Inc. + Copyright © 2015-2025 Bitwarden Inc. https://github.com/bitwarden/clients/ https://help.bitwarden.com/article/cli/ https://github.com/bitwarden/clients/issues diff --git a/apps/desktop/electron-builder.json b/apps/desktop/electron-builder.json index 925706bec7b..d51d9412d80 100644 --- a/apps/desktop/electron-builder.json +++ b/apps/desktop/electron-builder.json @@ -5,7 +5,7 @@ "productName": "Bitwarden", "appId": "com.bitwarden.desktop", "buildDependenciesFromSource": true, - "copyright": "Copyright © 2015-2024 Bitwarden Inc.", + "copyright": "Copyright © 2015-2025 Bitwarden Inc.", "directories": { "buildResources": "resources", "output": "dist", diff --git a/apps/desktop/stores/chocolatey/bitwarden.nuspec b/apps/desktop/stores/chocolatey/bitwarden.nuspec index dc95703614d..450fa734736 100644 --- a/apps/desktop/stores/chocolatey/bitwarden.nuspec +++ b/apps/desktop/stores/chocolatey/bitwarden.nuspec @@ -10,7 +10,7 @@ Bitwarden Inc. https://bitwarden.com/ https://raw.githubusercontent.com/bitwarden/brand/master/icons/256x256.png - Copyright © 2015-2024 Bitwarden Inc. + Copyright © 2015-2025 Bitwarden Inc. https://github.com/bitwarden/clients/ https://bitwarden.com/help/ https://github.com/bitwarden/clients/issues From 116751d4caaa024424b508309fcf23cb82532619 Mon Sep 17 00:00:00 2001 From: Bryan Cunningham Date: Thu, 24 Apr 2025 15:34:29 -0400 Subject: [PATCH 047/499] add small button variant (#14326) * adds small button size variant * makes small icon button same size as small button * testing small button for extension header * remove extension changes * update popout layout story * revert change to small icon button padding * add whitespace to see if error resolves * default buttonType to primary * default buttonType to secondary * add comment around why nonNullButtonSize value exists * add comment to property about using the non null version * Update apps/browser/src/platform/popup/layout/popup-layout.stories.ts Co-authored-by: Oscar Hinton * updated input syntax when using static values * remove nonNull value coersion * allow changing of size input in Story --------- Co-authored-by: Oscar Hinton --- .../popup/layout/popup-layout.stories.ts | 2 +- .../components/src/button/button.component.ts | 20 ++++---- libs/components/src/button/button.stories.ts | 47 ++++++++++++++----- .../src/shared/button-like.abstraction.ts | 2 + 4 files changed, 50 insertions(+), 21 deletions(-) diff --git a/apps/browser/src/platform/popup/layout/popup-layout.stories.ts b/apps/browser/src/platform/popup/layout/popup-layout.stories.ts index a4c6b894159..32c4f151a8f 100644 --- a/apps/browser/src/platform/popup/layout/popup-layout.stories.ts +++ b/apps/browser/src/platform/popup/layout/popup-layout.stories.ts @@ -80,7 +80,7 @@ class VaultComponent { @Component({ selector: "mock-add-button", template: ` - diff --git a/libs/components/src/button/button.component.ts b/libs/components/src/button/button.component.ts index 0b4ce3073c3..19618938c42 100644 --- a/libs/components/src/button/button.component.ts +++ b/libs/components/src/button/button.component.ts @@ -1,12 +1,10 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { coerceBooleanProperty } from "@angular/cdk/coercion"; import { NgClass } from "@angular/common"; -import { Input, HostBinding, Component, model, computed } from "@angular/core"; +import { Input, HostBinding, Component, model, computed, input } from "@angular/core"; import { toObservable, toSignal } from "@angular/core/rxjs-interop"; import { debounce, interval } from "rxjs"; -import { ButtonLikeAbstraction, ButtonType } from "../shared/button-like.abstraction"; +import { ButtonLikeAbstraction, ButtonType, ButtonSize } from "../shared/button-like.abstraction"; const focusRing = [ "focus-visible:tw-ring-2", @@ -15,6 +13,11 @@ const focusRing = [ "focus-visible:tw-z-10", ]; +const buttonSizeStyles: Record = { + small: ["tw-py-1", "tw-px-3", "tw-text-sm"], + default: ["tw-py-1.5", "tw-px-3"], +}; + const buttonStyles: Record = { primary: [ "tw-border-primary-600", @@ -59,8 +62,6 @@ export class ButtonComponent implements ButtonLikeAbstraction { @HostBinding("class") get classList() { return [ "tw-font-semibold", - "tw-py-1.5", - "tw-px-3", "tw-rounded-full", "tw-transition", "tw-border-2", @@ -85,7 +86,8 @@ export class ButtonComponent implements ButtonLikeAbstraction { "disabled:hover:tw-no-underline", ] : [], - ); + ) + .concat(buttonSizeStyles[this.size() || "default"]); } protected disabledAttr = computed(() => { @@ -105,7 +107,9 @@ export class ButtonComponent implements ButtonLikeAbstraction { return this.showLoadingStyle() || (this.disabledAttr() && this.loading() === false); }); - @Input() buttonType: ButtonType; + @Input() buttonType: ButtonType = "secondary"; + + size = input("default"); private _block = false; diff --git a/libs/components/src/button/button.stories.ts b/libs/components/src/button/button.stories.ts index 448e290cce8..759bd1a352c 100644 --- a/libs/components/src/button/button.stories.ts +++ b/libs/components/src/button/button.stories.ts @@ -9,6 +9,13 @@ export default { buttonType: "primary", disabled: false, loading: false, + size: "default", + }, + argTypes: { + size: { + options: ["small", "default"], + control: { type: "radio" }, + }, }, parameters: { design: { @@ -24,19 +31,19 @@ export const Primary: Story = { render: (args) => ({ props: args, template: /*html*/ ` -
- - - - - +
+ + + + +
-
- Anchor - Anchor:hover - Anchor:focus-visible - Anchor:hover:focus-visible - Anchor:active + `, }), @@ -59,6 +66,22 @@ export const Danger: Story = { }, }; +export const Small: Story = { + render: (args) => ({ + props: args, + template: /*html*/ ` +
+ + + +
+ `, + }), + args: { + size: "small", + }, +}; + export const Loading: Story = { render: (args) => ({ props: args, diff --git a/libs/components/src/shared/button-like.abstraction.ts b/libs/components/src/shared/button-like.abstraction.ts index 5ee9d272594..c7cb620bff0 100644 --- a/libs/components/src/shared/button-like.abstraction.ts +++ b/libs/components/src/shared/button-like.abstraction.ts @@ -4,6 +4,8 @@ import { ModelSignal } from "@angular/core"; // @ts-strict-ignore export type ButtonType = "primary" | "secondary" | "danger" | "unstyled"; +export type ButtonSize = "default" | "small"; + export abstract class ButtonLikeAbstraction { loading: ModelSignal; disabled: ModelSignal; From 4a01c8bb171da95746c9d37015098e1164370b0a Mon Sep 17 00:00:00 2001 From: Daniel Riera Date: Thu, 24 Apr 2025 16:15:27 -0400 Subject: [PATCH 048/499] PM-20391 UX: Saving new login when none exist (#14406) * PM-20391 UX: Saving new login when none exist * Update apps/browser/src/_locales/en/messages.json Co-authored-by: Jonathan Prusik * Update apps/browser/src/_locales/en/messages.json Co-authored-by: Jonathan Prusik * Update apps/browser/src/autofill/notification/bar.ts Co-authored-by: Jonathan Prusik * Update apps/browser/src/autofill/content/components/cipher/cipher-action.ts Co-authored-by: Jonathan Prusik --------- Co-authored-by: Jonathan Prusik --- apps/browser/src/_locales/en/messages.json | 46 ++++---- .../background/notification.background.ts | 100 +++++++++++++----- .../content/components/buttons/edit-button.ts | 1 + .../components/cipher/cipher-action.ts | 2 +- .../content/components/icons/pencil-square.ts | 2 +- .../components/notification/container.ts | 4 +- apps/browser/src/autofill/notification/bar.ts | 5 +- 7 files changed, 104 insertions(+), 56 deletions(-) diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index d1d05a4e852..4f83b07506b 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -1071,6 +1071,10 @@ }, "description": "Aria label for the view button in notification bar confirmation message" }, + "notificationEditTooltip": { + "message": "Edit before saving", + "description": "Tooltip and Aria label for edit button on cipher item" + }, "newNotification": { "message": "New notification" }, @@ -1110,12 +1114,12 @@ "message": "Update login", "description": "Button text for updating an existing login entry." }, - "saveLoginPrompt": { - "message": "Save login?", + "saveLogin": { + "message": "Save login", "description": "Prompt asking the user if they want to save their login details." }, - "updateLoginPrompt": { - "message": "Update existing login?", + "updateLogin": { + "message": "Update existing login", "description": "Prompt asking the user if they want to update an existing login entry." }, "loginSaveSuccess": { @@ -1128,24 +1132,24 @@ }, "loginUpdateTaskSuccess": { "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", - "placeholders": { - "organization": { - "content": "$1" - } - }, - "description": "Shown to user after login is updated." + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." }, "loginUpdateTaskSuccessAdditional": { "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", - "placeholders": { - "organization": { - "content": "$1" - }, - "task_count": { - "content": "$2" - } + "placeholders": { + "organization": { + "content": "$1" }, - "description": "Shown to user after login is updated." + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." }, "nextSecurityTaskAction": { "message": "Change next password", @@ -2518,8 +2522,8 @@ "example": "Acme Corp" }, "count": { - "content": "$2", - "example": "2" + "content": "$2", + "example": "2" } } }, @@ -5224,4 +5228,4 @@ "secureDevicesBody": { "message": "Save unlimited passwords across unlimited devices with Bitwarden mobile, browser, and desktop apps." } -} +} \ No newline at end of file diff --git a/apps/browser/src/autofill/background/notification.background.ts b/apps/browser/src/autofill/background/notification.background.ts index 0b9c6244987..9083f15d4f2 100644 --- a/apps/browser/src/autofill/background/notification.background.ts +++ b/apps/browser/src/autofill/background/notification.background.ts @@ -163,6 +163,7 @@ export default class NotificationBackground { * Gets the current active tab and retrieves the relevant decrypted cipher * for the tab's URL. It constructs and returns an array of `NotificationCipherData` objects or a singular object. * If no active tab or URL is found, it returns an empty array. + * If new login, returns a preview of the cipher. * * @returns {Promise} */ @@ -175,53 +176,94 @@ export default class NotificationBackground { firstValueFrom(this.accountService.activeAccount$.pipe(getOptionalUserId)), ]); + if (!currentTab?.url || !activeUserId) { + return []; + } + const [decryptedCiphers, organizations] = await Promise.all([ - this.cipherService.getAllDecryptedForUrl(currentTab?.url, activeUserId), + this.cipherService.getAllDecryptedForUrl(currentTab.url, activeUserId), firstValueFrom(this.organizationService.organizations$(activeUserId)), ]); const iconsServerUrl = env.getIconsUrl(); - const toNotificationData = (view: CipherView): NotificationCipherData => { - const { id, name, reprompt, favorite, login, organizationId } = view; + const getOrganizationType = (orgId?: string) => + organizations.find((org) => org.id === orgId)?.productTierType; - const type = organizations.find((org) => org.id === organizationId)?.productTierType; + const cipherQueueMessage = this.notificationQueue.find( + (message): message is AddChangePasswordQueueMessage | AddLoginQueueMessage => + message.type === NotificationQueueMessageType.ChangePassword || + message.type === NotificationQueueMessageType.AddLogin, + ); - const organizationCategories: OrganizationCategory[] = []; + if (cipherQueueMessage) { + const cipherView = + cipherQueueMessage.type === NotificationQueueMessageType.ChangePassword + ? await this.getDecryptedCipherById(cipherQueueMessage.cipherId, activeUserId) + : this.convertAddLoginQueueMessageToCipherView(cipherQueueMessage); + + const organizationType = getOrganizationType(cipherView.organizationId); + return [ + this.convertToNotificationCipherData( + cipherView, + iconsServerUrl, + showFavicons, + organizationType, + ), + ]; + } + + return decryptedCiphers.map((view) => + this.convertToNotificationCipherData( + view, + iconsServerUrl, + showFavicons, + getOrganizationType(view.organizationId), + ), + ); + } + + /** + * Converts a CipherView and organization type into a NotificationCipherData object + * for use in the notification bar. + * + * @returns A NotificationCipherData object containing the relevant cipher information. + */ + + convertToNotificationCipherData( + view: CipherView, + iconsServerUrl: string, + showFavicons: boolean, + organizationType?: ProductTierType, + ): NotificationCipherData { + const { id, name, reprompt, favorite, login } = view; + + const organizationCategories: OrganizationCategory[] = []; + + if (organizationType != null) { if ( [ProductTierType.Teams, ProductTierType.Enterprise, ProductTierType.TeamsStarter].includes( - type, + organizationType, ) ) { organizationCategories.push(OrganizationCategories.business); } - if ([ProductTierType.Families, ProductTierType.Free].includes(type)) { + + if ([ProductTierType.Families, ProductTierType.Free].includes(organizationType)) { organizationCategories.push(OrganizationCategories.family); } - - return { - id, - name, - type: CipherType.Login, - reprompt, - favorite, - ...(organizationCategories.length ? { organizationCategories } : {}), - icon: buildCipherIcon(iconsServerUrl, view, showFavicons), - login: login && { username: login.username }, - }; - }; - - const changeItem = this.notificationQueue.find( - (message): message is AddChangePasswordQueueMessage => - message.type === NotificationQueueMessageType.ChangePassword, - ); - - if (changeItem) { - const cipherView = await this.getDecryptedCipherById(changeItem.cipherId, activeUserId); - return [toNotificationData(cipherView)]; } - return decryptedCiphers.map(toNotificationData); + return { + id, + name, + type: CipherType.Login, + reprompt, + favorite, + ...(organizationCategories.length ? { organizationCategories } : {}), + icon: buildCipherIcon(iconsServerUrl, view, showFavicons), + login: login && { username: login.username }, + }; } /** diff --git a/apps/browser/src/autofill/content/components/buttons/edit-button.ts b/apps/browser/src/autofill/content/components/buttons/edit-button.ts index 67221f5be18..a0037146db2 100644 --- a/apps/browser/src/autofill/content/components/buttons/edit-button.ts +++ b/apps/browser/src/autofill/content/components/buttons/edit-button.ts @@ -21,6 +21,7 @@ export function EditButton({ -
-
- -
- -
- - -
-
- -
- -
-
- - -
- - -
-
-
-
- - - {{ "newCustomField" | i18n }} - -
-
- - -
-
-
diff --git a/apps/web/src/app/vault/individual-vault/add-edit-custom-fields.component.ts b/apps/web/src/app/vault/individual-vault/add-edit-custom-fields.component.ts deleted file mode 100644 index b492de85caa..00000000000 --- a/apps/web/src/app/vault/individual-vault/add-edit-custom-fields.component.ts +++ /dev/null @@ -1,20 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Component, Input } from "@angular/core"; - -import { AddEditCustomFieldsComponent as BaseAddEditCustomFieldsComponent } from "@bitwarden/angular/vault/components/add-edit-custom-fields.component"; -import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; - -@Component({ - selector: "app-vault-add-edit-custom-fields", - templateUrl: "add-edit-custom-fields.component.html", -}) -export class AddEditCustomFieldsComponent extends BaseAddEditCustomFieldsComponent { - @Input() viewOnly: boolean; - @Input() copy: (value: string, typeI18nKey: string, aType: string) => void; - - constructor(i18nService: I18nService, eventCollectionService: EventCollectionService) { - super(i18nService, eventCollectionService); - } -} diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 05d29071731..bc87ec05fbd 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -422,18 +422,9 @@ "folder": { "message": "Folder" }, - "newCustomField": { - "message": "New custom field" - }, "value": { "message": "Value" }, - "dragToSort": { - "message": "Drag to sort" - }, - "dragToReorder": { - "message": "Drag to reorder" - }, "cfTypeText": { "message": "Text" }, From 17bc3e6501ef0def424c7d746c288f7ab5d0d276 Mon Sep 17 00:00:00 2001 From: Daniel Riera Date: Mon, 28 Apr 2025 14:00:31 -0400 Subject: [PATCH 069/499] PM-20546 pass folder to openAddEditVaultItemPopout (#14524) --- .../src/autofill/background/notification.background.ts | 3 ++- apps/browser/src/autofill/notification/bar.ts | 8 ++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/apps/browser/src/autofill/background/notification.background.ts b/apps/browser/src/autofill/background/notification.background.ts index 4e2a99d4a7a..339b033809d 100644 --- a/apps/browser/src/autofill/background/notification.background.ts +++ b/apps/browser/src/autofill/background/notification.background.ts @@ -801,7 +801,7 @@ export default class NotificationBackground { message: NotificationBackgroundExtensionMessage, senderTab: chrome.tabs.Tab, ) { - const { cipherId, organizationId } = message; + const { cipherId, organizationId, folder } = message; const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getOptionalUserId)); if (cipherId) { await this.openAddEditVaultItemPopout(senderTab, { cipherId }); @@ -813,6 +813,7 @@ export default class NotificationBackground { if (queueItem?.type === NotificationQueueMessageType.AddLogin) { const cipherView = this.convertAddLoginQueueMessageToCipherView(queueItem); cipherView.organizationId = organizationId; + cipherView.folderId = folder; if (userId) { await this.cipherService.setAddEditCipherInfo({ cipher: cipherView }, userId); diff --git a/apps/browser/src/autofill/notification/bar.ts b/apps/browser/src/autofill/notification/bar.ts index 14d9bcd6d0f..d9a5c937f9c 100644 --- a/apps/browser/src/autofill/notification/bar.ts +++ b/apps/browser/src/autofill/notification/bar.ts @@ -263,16 +263,16 @@ function handleCloseNotification(e: Event) { function handleSaveAction(e: Event) { const selectedVault = selectedVaultSignal.get(); + const selectedFolder = selectedFolderSignal.get(); + if (selectedVault.length > 1) { - openAddEditVaultItemPopout(e, { organizationId: selectedVault }); + openAddEditVaultItemPopout(e, { organizationId: selectedVault, folder: selectedFolder }); handleCloseNotification(e); return; } e.preventDefault(); - const selectedFolder = selectedFolderSignal.get(); - sendSaveCipherMessage(removeIndividualVault(), selectedFolder); if (removeIndividualVault()) { return; @@ -370,7 +370,7 @@ function handleSaveCipherAttemptCompletedMessage(message: NotificationBarWindowM function openAddEditVaultItemPopout( e: Event, - options: { cipherId?: string; organizationId?: string }, + options: { cipherId?: string; organizationId?: string; folder?: string }, ) { e.preventDefault(); sendPlatformMessage({ From ff846d2fab12ae6c6425fafef4e7a5fc473a55af Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Mon, 28 Apr 2025 14:38:48 -0400 Subject: [PATCH 070/499] [PM-20300] Fix self-serve resubscribe bugs (#14296) * Address bugs * Cy's feedback * Cy's feedback --- .../billing/organizations/change-plan-dialog.component.html | 2 +- .../billing/organizations/change-plan-dialog.component.ts | 2 +- .../organization-subscription-cloud.component.ts | 6 +++++- apps/web/src/locales/en/messages.json | 3 +++ 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/apps/web/src/app/billing/organizations/change-plan-dialog.component.html b/apps/web/src/app/billing/organizations/change-plan-dialog.component.html index 64a694cdef0..464890809f5 100644 --- a/apps/web/src/app/billing/organizations/change-plan-dialog.component.html +++ b/apps/web/src/app/billing/organizations/change-plan-dialog.component.html @@ -983,7 +983,7 @@
{{ "upgradeEventLogMessage" | i18n }} @@ -125,10 +125,10 @@

- {{ "limitedEventLogs" | i18n: ProductTierType[organization?.productTierType] }} + {{ "upgradeEventLogTitleMessage" | i18n }}

- {{ "upgradeForFullEvents" | i18n }} + {{ "upgradeForFullEventsMessage" | i18n }}

diff --git a/apps/browser/src/autofill/content/components/notification/footer.ts b/apps/browser/src/autofill/content/components/notification/footer.ts index 40c3dcecf41..baa1a2ecffc 100644 --- a/apps/browser/src/autofill/content/components/notification/footer.ts +++ b/apps/browser/src/autofill/content/components/notification/footer.ts @@ -18,6 +18,7 @@ export type NotificationFooterProps = { i18n: { [key: string]: string }; notificationType?: NotificationType; organizations?: OrgView[]; + personalVaultIsAllowed: boolean; theme: Theme; handleSaveAction: (e: Event) => void; }; @@ -28,6 +29,7 @@ export function NotificationFooter({ i18n, notificationType, organizations, + personalVaultIsAllowed, theme, handleSaveAction, }: NotificationFooterProps) { @@ -46,6 +48,7 @@ export function NotificationFooter({ handlePrimaryButtonClick: handleSaveAction, text: primaryButtonText, }, + personalVaultIsAllowed, theme, }) : nothing} diff --git a/apps/browser/src/autofill/notification/bar.ts b/apps/browser/src/autofill/notification/bar.ts index d9a5c937f9c..162912c5596 100644 --- a/apps/browser/src/autofill/notification/bar.ts +++ b/apps/browser/src/autofill/notification/bar.ts @@ -135,7 +135,11 @@ async function initNotificationBar(message: NotificationBarWindowMessage) { } notificationBarIframeInitData = initData; - const { isVaultLocked, theme } = notificationBarIframeInitData; + const { + isVaultLocked, + removeIndividualVault: personalVaultDisallowed, // renamed to avoid local method collision + theme, + } = notificationBarIframeInitData; const i18n = getI18n(); const resolvedTheme = getResolvedTheme(theme ?? ThemeTypes.Light); @@ -172,6 +176,7 @@ async function initNotificationBar(message: NotificationBarWindowMessage) { ...notificationBarIframeInitData, type: notificationBarIframeInitData.type as NotificationType, theme: resolvedTheme, + personalVaultIsAllowed: !personalVaultDisallowed, handleCloseNotification, handleSaveAction, handleEditOrUpdateAction, @@ -266,7 +271,10 @@ function handleSaveAction(e: Event) { const selectedFolder = selectedFolderSignal.get(); if (selectedVault.length > 1) { - openAddEditVaultItemPopout(e, { organizationId: selectedVault, folder: selectedFolder }); + openAddEditVaultItemPopout(e, { + organizationId: selectedVault, + folder: selectedFolder, + }); handleCloseNotification(e); return; } @@ -370,7 +378,11 @@ function handleSaveCipherAttemptCompletedMessage(message: NotificationBarWindowM function openAddEditVaultItemPopout( e: Event, - options: { cipherId?: string; organizationId?: string; folder?: string }, + options: { + cipherId?: string; + organizationId?: string; + folder?: string; + }, ) { e.preventDefault(); sendPlatformMessage({ From 417b59a1ccaa813876a350db1fbe6f076b94f065 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 29 Apr 2025 11:44:33 -0400 Subject: [PATCH 080/499] [deps] Platform: Update @types/node to v22.14.1 (#14481) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Daniel García --- .../package-lock.json | 16 ++++++++-------- .../native-messaging-test-runner/package.json | 2 +- package-lock.json | 16 ++++++++-------- package.json | 2 +- 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/apps/desktop/native-messaging-test-runner/package-lock.json b/apps/desktop/native-messaging-test-runner/package-lock.json index 02a5e850401..46cb92051b0 100644 --- a/apps/desktop/native-messaging-test-runner/package-lock.json +++ b/apps/desktop/native-messaging-test-runner/package-lock.json @@ -17,7 +17,7 @@ "yargs": "17.7.2" }, "devDependencies": { - "@types/node": "22.10.7", + "@types/node": "22.14.1", "typescript": "5.4.2" } }, @@ -101,12 +101,12 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.10.7", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.7.tgz", - "integrity": "sha512-V09KvXxFiutGp6B7XkpaDXlNadZxrzajcY50EuoLIpQ6WWYCSvf19lVIazzfIzQvhUN2HjX12spLojTnhuKlGg==", + "version": "22.14.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.1.tgz", + "integrity": "sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw==", "license": "MIT", "dependencies": { - "undici-types": "~6.20.0" + "undici-types": "~6.21.0" } }, "node_modules/acorn": { @@ -347,9 +347,9 @@ } }, "node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "license": "MIT" }, "node_modules/uuid": { diff --git a/apps/desktop/native-messaging-test-runner/package.json b/apps/desktop/native-messaging-test-runner/package.json index 0df272c142f..8a91c48eb11 100644 --- a/apps/desktop/native-messaging-test-runner/package.json +++ b/apps/desktop/native-messaging-test-runner/package.json @@ -22,7 +22,7 @@ "yargs": "17.7.2" }, "devDependencies": { - "@types/node": "22.10.7", + "@types/node": "22.14.1", "typescript": "5.4.2" }, "_moduleAliases": { diff --git a/package-lock.json b/package-lock.json index 23ed852b06a..3a66efd8092 100644 --- a/package-lock.json +++ b/package-lock.json @@ -110,7 +110,7 @@ "@types/koa-json": "2.0.23", "@types/lowdb": "1.0.15", "@types/lunr": "2.3.7", - "@types/node": "22.10.7", + "@types/node": "22.14.1", "@types/node-fetch": "2.6.4", "@types/node-forge": "1.3.11", "@types/papaparse": "5.3.15", @@ -11649,13 +11649,13 @@ } }, "node_modules/@types/node": { - "version": "22.10.7", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.7.tgz", - "integrity": "sha512-V09KvXxFiutGp6B7XkpaDXlNadZxrzajcY50EuoLIpQ6WWYCSvf19lVIazzfIzQvhUN2HjX12spLojTnhuKlGg==", + "version": "22.14.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.1.tgz", + "integrity": "sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~6.20.0" + "undici-types": "~6.21.0" } }, "node_modules/@types/node-fetch": { @@ -35688,9 +35688,9 @@ } }, "node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "dev": true, "license": "MIT" }, diff --git a/package.json b/package.json index 02d81c5fddc..8aa0b618aec 100644 --- a/package.json +++ b/package.json @@ -72,7 +72,7 @@ "@types/koa-json": "2.0.23", "@types/lowdb": "1.0.15", "@types/lunr": "2.3.7", - "@types/node": "22.10.7", + "@types/node": "22.14.1", "@types/node-fetch": "2.6.4", "@types/node-forge": "1.3.11", "@types/papaparse": "5.3.15", From 29d0e74e238a9db92e8000964d200d4b64c7fe01 Mon Sep 17 00:00:00 2001 From: Todd Martin <106564991+trmartin4@users.noreply.github.com> Date: Tue, 29 Apr 2025 12:00:02 -0400 Subject: [PATCH 081/499] fix(login): [PM-20587] Fix unawaited calls to set login email * Await setting login email in state. * Changed to get email state within the component. * Added null filter * PM-20587 - LoginViaAuthRequest component - update initStandardAuthRequestFlow to correctly retrieve data from active account. --------- Co-authored-by: Jared Snider --- .../login-decryption-options.component.ts | 2 -- .../login-via-auth-request.component.ts | 23 +++++++++++++++++-- .../auth/src/angular/login/login.component.ts | 2 +- .../password-hint/password-hint.component.ts | 2 +- 4 files changed, 23 insertions(+), 6 deletions(-) diff --git a/libs/auth/src/angular/login-decryption-options/login-decryption-options.component.ts b/libs/auth/src/angular/login-decryption-options/login-decryption-options.component.ts index 62fbeae26b6..945e6bbaaf5 100644 --- a/libs/auth/src/angular/login-decryption-options/login-decryption-options.component.ts +++ b/libs/auth/src/angular/login-decryption-options/login-decryption-options.component.ts @@ -284,7 +284,6 @@ export class LoginDecryptionOptionsComponent implements OnInit { } protected async approveFromOtherDevice() { - this.loginEmailService.setLoginEmail(this.email); await this.router.navigate(["/login-with-device"]); } @@ -297,7 +296,6 @@ export class LoginDecryptionOptionsComponent implements OnInit { } protected async requestAdminApproval() { - this.loginEmailService.setLoginEmail(this.email); await this.router.navigate(["/admin-approval-requested"]); } 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 0af52e02b84..5de2339bda1 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 @@ -2,7 +2,7 @@ import { CommonModule } from "@angular/common"; import { Component, OnDestroy, OnInit } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { IsActiveMatchOptions, Router, RouterModule } from "@angular/router"; -import { firstValueFrom, map } from "rxjs"; +import { Observable, filter, firstValueFrom, map, merge, race, take, timer } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { @@ -178,7 +178,26 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy { private async initStandardAuthRequestFlow(): Promise { this.flow = Flow.StandardAuthRequest; - this.email = (await firstValueFrom(this.loginEmailService.loginEmail$)) || undefined; + // For a standard flow, we can get the user's email from two different places: + // 1. The loginEmailService, which is the email that the user is trying to log in with. This is cleared + // when the user logs in successfully. We can use this when the user is using Login with Device. + // 2. With TDE Login with Another Device, the user is already logged in and we just need to get + // a decryption key, so we can use the active account's email. + const activeAccountEmail$: Observable = + this.accountService.activeAccount$.pipe(map((a) => a?.email)); + const loginEmail$: Observable = this.loginEmailService.loginEmail$; + + // Use merge as we want to get the first value from either observable. + const firstEmail$ = merge(loginEmail$, activeAccountEmail$).pipe( + filter((e): e is string => !!e), // convert null/undefined to false and filter out so we narrow type to string + take(1), // complete after first value + ); + + const emailRetrievalTimeout$ = timer(2500).pipe(map(() => undefined as undefined)); + + // Wait for either the first email or the timeout to occur so we can proceed + // neither above observable will complete, so we have to add a timeout + this.email = await firstValueFrom(race(firstEmail$, emailRetrievalTimeout$)); if (!this.email) { await this.handleMissingEmail(); diff --git a/libs/auth/src/angular/login/login.component.ts b/libs/auth/src/angular/login/login.component.ts index 4ca18b4985e..eb2bdcee291 100644 --- a/libs/auth/src/angular/login/login.component.ts +++ b/libs/auth/src/angular/login/login.component.ts @@ -539,7 +539,7 @@ export class LoginComponent implements OnInit, OnDestroy { // If we load an email into the form, we need to initialize it for the login process as well // so that other login components can use it. // We do this here as it's possible that a user doesn't edit the email field before submitting. - this.loginEmailService.setLoginEmail(storedEmail); + await this.loginEmailService.setLoginEmail(storedEmail); } else { this.formGroup.controls.rememberEmail.setValue(false); } diff --git a/libs/auth/src/angular/password-hint/password-hint.component.ts b/libs/auth/src/angular/password-hint/password-hint.component.ts index 996b4d8d92e..cf24c68e10d 100644 --- a/libs/auth/src/angular/password-hint/password-hint.component.ts +++ b/libs/auth/src/angular/password-hint/password-hint.component.ts @@ -79,7 +79,7 @@ export class PasswordHintComponent implements OnInit { }; protected async cancel() { - this.loginEmailService.setLoginEmail(this.email); + await this.loginEmailService.setLoginEmail(this.email); await this.router.navigate(["login"]); } From a35d004059c9f2f3f44aef72463620abb621497f Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Tue, 29 Apr 2025 12:03:33 -0400 Subject: [PATCH 082/499] Fix upgrade/change plan for free organization (#14537) --- .../change-plan-dialog.component.html | 2 +- .../change-plan-dialog.component.ts | 17 ++++++++++++++--- ...organization-subscription-cloud.component.ts | 2 +- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/apps/web/src/app/billing/organizations/change-plan-dialog.component.html b/apps/web/src/app/billing/organizations/change-plan-dialog.component.html index 464890809f5..7d898a1307b 100644 --- a/apps/web/src/app/billing/organizations/change-plan-dialog.component.html +++ b/apps/web/src/app/billing/organizations/change-plan-dialog.component.html @@ -983,7 +983,7 @@
+ + - - - - - - - - - {{ "addNewOrganization" | i18n }} - - + + diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-clients.component.ts b/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-clients.component.ts index 3322fd3a85b..a57e6351349 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-clients.component.ts +++ b/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-clients.component.ts @@ -15,8 +15,6 @@ import { Provider } from "@bitwarden/common/admin-console/models/domain/provider import { ProviderOrganizationOrganizationDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-organization.response"; import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions"; import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; import { @@ -73,9 +71,6 @@ export class ManageClientsComponent { protected searchControl = new FormControl("", { nonNullable: true }); protected plans: PlanResponse[] = []; - protected addExistingOrgsFromProviderPortal$ = this.configService.getFeatureFlag$( - FeatureFlag.PM15179_AddExistingOrgsFromProviderPortal, - ); pageTitle = this.i18nService.t("clients"); clientColumnHeader = this.i18nService.t("client"); @@ -91,7 +86,6 @@ export class ManageClientsComponent { private toastService: ToastService, private validationService: ValidationService, private webProviderService: WebProviderService, - private configService: ConfigService, private billingNotificationService: BillingNotificationService, ) { this.activatedRoute.queryParams.pipe(first(), takeUntilDestroyed()).subscribe((queryParams) => { diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index 4eabb8ea114..0e0956b0460 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", - PM15179_AddExistingOrgsFromProviderPortal = "pm-15179-add-existing-orgs-from-provider-portal", PM12276_BreadcrumbEventLogs = "pm-12276-breadcrumbing-for-business-features", PM18794_ProviderPaymentMethod = "pm-18794-provider-payment-method", PM17772_AdminInitiatedSponsorships = "pm-17772-admin-initiated-sponsorships", @@ -119,7 +118,6 @@ export const DefaultFeatureFlagValue = { /* Billing */ [FeatureFlag.TrialPaymentOptional]: FALSE, - [FeatureFlag.PM15179_AddExistingOrgsFromProviderPortal]: FALSE, [FeatureFlag.PM12276_BreadcrumbEventLogs]: FALSE, [FeatureFlag.PM18794_ProviderPaymentMethod]: FALSE, [FeatureFlag.PM17772_AdminInitiatedSponsorships]: FALSE, From 438f90c987a6d49397ce84935e09eadd823984e3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 29 Apr 2025 12:20:29 -0400 Subject: [PATCH 085/499] [deps] Autofill: Update tldts to v7 (#14510) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- apps/cli/package.json | 2 +- package-lock.json | 36 +++++++++++++++++++++++++++--------- package.json | 2 +- 3 files changed, 29 insertions(+), 11 deletions(-) diff --git a/apps/cli/package.json b/apps/cli/package.json index daec6593543..79d4786a23c 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -88,7 +88,7 @@ "papaparse": "5.5.2", "proper-lockfile": "4.1.2", "rxjs": "7.8.1", - "tldts": "6.1.77", + "tldts": "7.0.1", "zxcvbn": "4.4.2" } } diff --git a/package-lock.json b/package-lock.json index 3a66efd8092..0db5832feca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -68,7 +68,7 @@ "qrious": "4.0.2", "rxjs": "7.8.1", "tabbable": "6.2.0", - "tldts": "6.1.77", + "tldts": "7.0.1", "utf-8-validate": "6.0.5", "zone.js": "0.14.10", "zxcvbn": "4.4.2" @@ -222,7 +222,7 @@ "papaparse": "5.5.2", "proper-lockfile": "4.1.2", "rxjs": "7.8.1", - "tldts": "6.1.77", + "tldts": "7.0.1", "zxcvbn": "4.4.2" }, "bin": { @@ -24747,6 +24747,24 @@ "node": ">= 14" } }, + "node_modules/jsdom/node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/jsdom/node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "license": "MIT" + }, "node_modules/jsdom/node_modules/tough-cookie": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", @@ -34580,21 +34598,21 @@ } }, "node_modules/tldts": { - "version": "6.1.77", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.77.tgz", - "integrity": "sha512-lBpoWgy+kYmuXWQ83+R7LlJCnsd9YW8DGpZSHhrMl4b8Ly/1vzOie3OdtmUJDkKxcgRGOehDu5btKkty+JEe+g==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.1.tgz", + "integrity": "sha512-C3TdHZKykiDkxPIKUYCDWyYpcLQ8bDYvF/RGfH66UikQX3Kro7ij2/WGNYgp5EfxXB4+Tu5H728uAgYGNE1eaQ==", "license": "MIT", "dependencies": { - "tldts-core": "^6.1.77" + "tldts-core": "^7.0.1" }, "bin": { "tldts": "bin/cli.js" } }, "node_modules/tldts-core": { - "version": "6.1.78", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.78.tgz", - "integrity": "sha512-jS0svNsB99jR6AJBmfmEWuKIgz91Haya91Z43PATaeHJ24BkMoNRb/jlaD37VYjb0mYf6gRL/HOnvS1zEnYBiw==", + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.4.tgz", + "integrity": "sha512-9/IRbnIvUENGD6rg7m6Q9h/jH5ZL28hwjAhxrJx0AmcBue1FSsc84XZFaV748EsDVflid86aGDR11eSz6sbQjA==", "license": "MIT" }, "node_modules/tmp": { diff --git a/package.json b/package.json index 8aa0b618aec..0af4c01e450 100644 --- a/package.json +++ b/package.json @@ -200,7 +200,7 @@ "qrious": "4.0.2", "rxjs": "7.8.1", "tabbable": "6.2.0", - "tldts": "6.1.77", + "tldts": "7.0.1", "utf-8-validate": "6.0.5", "zone.js": "0.14.10", "zxcvbn": "4.4.2" From bcbce8385c8acb5059a7d560f24d1ab6564122ea Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 29 Apr 2025 12:25:49 -0400 Subject: [PATCH 086/499] [deps]: Update uuid to v11.1.0 (#14508) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .../native-messaging-test-runner/package-lock.json | 8 ++++---- apps/desktop/native-messaging-test-runner/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/desktop/native-messaging-test-runner/package-lock.json b/apps/desktop/native-messaging-test-runner/package-lock.json index 46cb92051b0..d506e109e94 100644 --- a/apps/desktop/native-messaging-test-runner/package-lock.json +++ b/apps/desktop/native-messaging-test-runner/package-lock.json @@ -13,7 +13,7 @@ "@bitwarden/node": "file:../../../libs/node", "module-alias": "2.2.3", "ts-node": "10.9.2", - "uuid": "11.0.5", + "uuid": "11.1.0", "yargs": "17.7.2" }, "devDependencies": { @@ -353,9 +353,9 @@ "license": "MIT" }, "node_modules/uuid": { - "version": "11.0.5", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.5.tgz", - "integrity": "sha512-508e6IcKLrhxKdBbcA2b4KQZlLVp2+J5UwQ6F7Drckkc5N9ZJwFa4TgWtsww9UG8fGHbm6gbV19TdM5pQ4GaIA==", + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" diff --git a/apps/desktop/native-messaging-test-runner/package.json b/apps/desktop/native-messaging-test-runner/package.json index 8a91c48eb11..f67ab259d3b 100644 --- a/apps/desktop/native-messaging-test-runner/package.json +++ b/apps/desktop/native-messaging-test-runner/package.json @@ -18,7 +18,7 @@ "@bitwarden/node": "file:../../../libs/node", "module-alias": "2.2.3", "ts-node": "10.9.2", - "uuid": "11.0.5", + "uuid": "11.1.0", "yargs": "17.7.2" }, "devDependencies": { From 751169d728e2a884e4066157dc81b596c54df1c3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 29 Apr 2025 10:03:38 -0700 Subject: [PATCH 087/499] [deps] Platform: Update Rust crate libc to v0.2.172 (#14456) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- apps/desktop/desktop_native/Cargo.lock | 4 ++-- apps/desktop/desktop_native/Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/desktop/desktop_native/Cargo.lock b/apps/desktop/desktop_native/Cargo.lock index bf7fd9dcc66..c225dc49f73 100644 --- a/apps/desktop/desktop_native/Cargo.lock +++ b/apps/desktop/desktop_native/Cargo.lock @@ -1491,9 +1491,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.169" +version = "0.2.172" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" +checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" [[package]] name = "libloading" diff --git a/apps/desktop/desktop_native/Cargo.toml b/apps/desktop/desktop_native/Cargo.toml index e6022d2c347..d9e61124864 100644 --- a/apps/desktop/desktop_native/Cargo.toml +++ b/apps/desktop/desktop_native/Cargo.toml @@ -28,7 +28,7 @@ hex = "=0.4.3" homedir = "=0.3.4" interprocess = "=2.2.1" keytar = "=0.1.6" -libc = "=0.2.169" +libc = "=0.2.172" log = "=0.4.25" napi = "=2.16.15" napi-build = "=2.1.4" From d0b93c871a1396ed9c027ccd098073e9527a0ce9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 29 Apr 2025 13:30:05 -0400 Subject: [PATCH 088/499] [deps] Autofill: Update wait-on to v8.0.3 (#14450) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 22 ++++++++++++++++------ package.json | 2 +- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0db5832feca..dcfc0c475bc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -178,7 +178,7 @@ "typescript-strict-plugin": "2.4.4", "url": "0.11.4", "util": "0.12.5", - "wait-on": "8.0.2", + "wait-on": "8.0.3", "webpack": "5.97.1", "webpack-cli": "6.0.1", "webpack-dev-server": "5.2.0", @@ -36915,17 +36915,17 @@ } }, "node_modules/wait-on": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-8.0.2.tgz", - "integrity": "sha512-qHlU6AawrgAIHlueGQHQ+ETcPLAauXbnoTKl3RKq20W0T8x0DKVAo5xWIYjHSyvHxQlcYbFdR0jp4T9bDVITFA==", + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-8.0.3.tgz", + "integrity": "sha512-nQFqAFzZDeRxsu7S3C7LbuxslHhk+gnJZHyethuGKAn2IVleIbTB9I3vJSQiSR+DifUqmdzfPMoMPJfLqMF2vw==", "dev": true, "license": "MIT", "dependencies": { - "axios": "^1.7.9", + "axios": "^1.8.2", "joi": "^17.13.3", "lodash": "^4.17.21", "minimist": "^1.2.8", - "rxjs": "^7.8.1" + "rxjs": "^7.8.2" }, "bin": { "wait-on": "bin/wait-on" @@ -36934,6 +36934,16 @@ "node": ">=12.0.0" } }, + "node_modules/wait-on/node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, "node_modules/walk-up-path": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/walk-up-path/-/walk-up-path-3.0.1.tgz", diff --git a/package.json b/package.json index 0af4c01e450..ad30bae428f 100644 --- a/package.json +++ b/package.json @@ -140,7 +140,7 @@ "typescript-strict-plugin": "2.4.4", "url": "0.11.4", "util": "0.12.5", - "wait-on": "8.0.2", + "wait-on": "8.0.3", "webpack": "5.97.1", "webpack-cli": "6.0.1", "webpack-dev-server": "5.2.0", From 7f4b2cba4871d7e6be6219ae6d3b1f5baa07e384 Mon Sep 17 00:00:00 2001 From: Vicki League Date: Tue, 29 Apr 2025 13:59:45 -0400 Subject: [PATCH 089/499] [PM-19437] Realign multiselect with button for the access selector in SM (#14324) --- .../access-policy-selector.component.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/access-policy-selector.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/access-policy-selector.component.html index 454b497fcdb..c8a50175781 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/access-policy-selector.component.html +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/access-policy-selector.component.html @@ -1,4 +1,4 @@ -
+
{{ label }} @@ -97,7 +97,7 @@ {{ hint }} -
+
diff --git a/apps/browser/src/vault/popup/settings/folders-v2.component.html b/apps/browser/src/vault/popup/settings/folders-v2.component.html index 35a0fbec0a9..552547c0230 100644 --- a/apps/browser/src/vault/popup/settings/folders-v2.component.html +++ b/apps/browser/src/vault/popup/settings/folders-v2.component.html @@ -1,7 +1,13 @@ - diff --git a/libs/tools/send/send-ui/src/new-send-dropdown/new-send-dropdown.component.html b/libs/tools/send/send-ui/src/new-send-dropdown/new-send-dropdown.component.html index 344348f6a90..c39d95616f6 100644 --- a/libs/tools/send/send-ui/src/new-send-dropdown/new-send-dropdown.component.html +++ b/libs/tools/send/send-ui/src/new-send-dropdown/new-send-dropdown.component.html @@ -1,4 +1,4 @@ - From e596584e87109a1e96c311d550e06ff48ee88562 Mon Sep 17 00:00:00 2001 From: Vijay Oommen Date: Thu, 1 May 2025 08:03:32 -0500 Subject: [PATCH 097/499] [PM-20505] Weak-passwords-report: refresh rows after edit (#14401) --- .../reports/pages/cipher-report.component.ts | 8 +- .../pages/weak-passwords-report.component.ts | 146 ++++++++++++------ ...dmin-console-cipher-form-config.service.ts | 2 +- 3 files changed, 107 insertions(+), 49 deletions(-) diff --git a/apps/web/src/app/tools/reports/pages/cipher-report.component.ts b/apps/web/src/app/tools/reports/pages/cipher-report.component.ts index 0ad8a0a519c..ceda7b1c480 100644 --- a/apps/web/src/app/tools/reports/pages/cipher-report.component.ts +++ b/apps/web/src/app/tools/reports/pages/cipher-report.component.ts @@ -67,7 +67,7 @@ export class CipherReportComponent implements OnDestroy { protected i18nService: I18nService, private syncService: SyncService, private cipherFormConfigService: CipherFormConfigService, - private adminConsoleCipherFormConfigService: AdminConsoleCipherFormConfigService, + protected adminConsoleCipherFormConfigService: AdminConsoleCipherFormConfigService, ) { this.organizations$ = this.accountService.activeAccount$.pipe( getUserId, @@ -207,7 +207,7 @@ export class CipherReportComponent implements OnDestroy { // If the dialog was closed by deleting the cipher, refresh the report. if (result === VaultItemDialogResult.Deleted || result === VaultItemDialogResult.Saved) { - await this.load(); + await this.refresh(result, cipher); } } @@ -215,6 +215,10 @@ export class CipherReportComponent implements OnDestroy { this.allCiphers = []; } + protected async refresh(result: VaultItemDialogResult, cipher: CipherView) { + await this.load(); + } + protected async repromptCipher(c: CipherView) { return ( c.reprompt === CipherRepromptType.None || diff --git a/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.ts b/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.ts index f7631e37a7d..4144c9ac20f 100644 --- a/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.ts +++ b/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.ts @@ -1,18 +1,22 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { Component, OnInit } from "@angular/core"; +import { firstValueFrom } from "rxjs"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; +import { CipherId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { BadgeVariant, DialogService } from "@bitwarden/components"; import { CipherFormConfigService, PasswordRepromptService } from "@bitwarden/vault"; +import { VaultItemDialogResult } from "@bitwarden/web-vault/app/vault/components/vault-item-dialog/vault-item-dialog.component"; import { AdminConsoleCipherFormConfigService } from "../../../vault/org-vault/services/admin-console-cipher-form-config.service"; @@ -40,7 +44,7 @@ export class WeakPasswordsReportComponent extends CipherReportComponent implemen i18nService: I18nService, syncService: SyncService, cipherFormConfigService: CipherFormConfigService, - adminConsoleCipherFormConfigService: AdminConsoleCipherFormConfigService, + protected adminConsoleCipherFormConfigService: AdminConsoleCipherFormConfigService, ) { super( cipherService, @@ -66,62 +70,112 @@ export class WeakPasswordsReportComponent extends CipherReportComponent implemen this.findWeakPasswords(allCiphers); } - protected findWeakPasswords(ciphers: CipherView[]): void { - ciphers.forEach((ciph) => { - const { type, login, isDeleted, edit, viewPassword } = ciph; - if ( - type !== CipherType.Login || - login.password == null || - login.password === "" || - isDeleted || - (!this.organization && !edit) || - !viewPassword - ) { + protected async refresh(result: VaultItemDialogResult, cipher: CipherView) { + if (result === VaultItemDialogResult.Deleted) { + // remove the cipher from the list + this.weakPasswordCiphers = this.weakPasswordCiphers.filter((c) => c.id !== cipher.id); + this.filterCiphersByOrg(this.weakPasswordCiphers); + return; + } + + if (result == VaultItemDialogResult.Saved) { + const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + let updatedCipher = await this.cipherService.get(cipher.id, activeUserId); + + if (this.isAdminConsoleActive) { + updatedCipher = await this.adminConsoleCipherFormConfigService.getCipher( + cipher.id as CipherId, + this.organization, + ); + } + + const updatedCipherView = await updatedCipher.decrypt( + await this.cipherService.getKeyForCipherKeyDecryption(updatedCipher, activeUserId), + ); + // update the cipher views + const updatedReportResult = this.determineWeakPasswordScore(updatedCipherView); + const index = this.weakPasswordCiphers.findIndex((c) => c.id === updatedCipherView.id); + + if (updatedReportResult == null) { + // the password is no longer weak + // remove the cipher from the list + this.weakPasswordCiphers.splice(index, 1); + this.filterCiphersByOrg(this.weakPasswordCiphers); return; } - const hasUserName = this.isUserNameNotEmpty(ciph); - let userInput: string[] = []; - if (hasUserName) { - const atPosition = login.username.indexOf("@"); - if (atPosition > -1) { - userInput = userInput - .concat( - login.username - .substr(0, atPosition) - .trim() - .toLowerCase() - .split(/[^A-Za-z0-9]/), - ) - .filter((i) => i.length >= 3); - } else { - userInput = login.username - .trim() - .toLowerCase() - .split(/[^A-Za-z0-9]/) - .filter((i) => i.length >= 3); - } + if (index > -1) { + // update the existing cipher + this.weakPasswordCiphers[index] = updatedReportResult; + this.filterCiphersByOrg(this.weakPasswordCiphers); } - const result = this.passwordStrengthService.getPasswordStrength( - login.password, - null, - userInput.length > 0 ? userInput : null, - ); + } + } - if (result.score != null && result.score <= 2) { - const scoreValue = this.scoreKey(result.score); - const row = { - ...ciph, - score: result.score, - reportValue: scoreValue, - scoreKey: scoreValue.sortOrder, - } as ReportResult; + protected findWeakPasswords(ciphers: CipherView[]): void { + ciphers.forEach((ciph) => { + const row = this.determineWeakPasswordScore(ciph); + if (row != null) { this.weakPasswordCiphers.push(row); } }); this.filterCiphersByOrg(this.weakPasswordCiphers); } + 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 + ) { + return; + } + + const hasUserName = this.isUserNameNotEmpty(ciph); + let userInput: string[] = []; + if (hasUserName) { + const atPosition = login.username.indexOf("@"); + if (atPosition > -1) { + userInput = userInput + .concat( + login.username + .substr(0, atPosition) + .trim() + .toLowerCase() + .split(/[^A-Za-z0-9]/), + ) + .filter((i) => i.length >= 3); + } else { + userInput = login.username + .trim() + .toLowerCase() + .split(/[^A-Za-z0-9]/) + .filter((i) => i.length >= 3); + } + } + const result = this.passwordStrengthService.getPasswordStrength( + login.password, + null, + userInput.length > 0 ? userInput : null, + ); + + if (result.score != null && result.score <= 2) { + const scoreValue = this.scoreKey(result.score); + return { + ...ciph, + score: result.score, + reportValue: scoreValue, + scoreKey: scoreValue.sortOrder, + } as ReportResult; + } + + return null; + } + protected canManageCipher(c: CipherView): boolean { // this will only ever be false from the org view; return true; diff --git a/apps/web/src/app/vault/org-vault/services/admin-console-cipher-form-config.service.ts b/apps/web/src/app/vault/org-vault/services/admin-console-cipher-form-config.service.ts index dd9cef91a54..15af27ba8d0 100644 --- a/apps/web/src/app/vault/org-vault/services/admin-console-cipher-form-config.service.ts +++ b/apps/web/src/app/vault/org-vault/services/admin-console-cipher-form-config.service.ts @@ -100,7 +100,7 @@ export class AdminConsoleCipherFormConfigService implements CipherFormConfigServ }; } - private async getCipher(id: CipherId | null, organization: Organization): Promise { + async getCipher(id: CipherId | null, organization: Organization): Promise { if (id == null) { return null; } From abf7c949d98e9213cf37ae7339b9ab88031bb6ef Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Thu, 1 May 2025 15:22:18 +0200 Subject: [PATCH 098/499] Move additional linting to architecture (#14580) --- .github/renovate.json5 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/renovate.json5 b/.github/renovate.json5 index ee97f16b0a9..91b4ac86328 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -149,6 +149,8 @@ { matchPackageNames: [ "@angular-eslint/schematics", + "@typescript-eslint/rule-tester", + "@typescript-eslint/utils", "angular-eslint", "eslint-config-prettier", "eslint-import-resolver-typescript", @@ -313,8 +315,6 @@ "@storybook/angular", "@storybook/manager-api", "@storybook/theming", - "@typescript-eslint/utils", - "@typescript-eslint/rule-tester", "@types/react", "autoprefixer", "bootstrap", From 1d004950785d9b003fd475a7e3e7f2c0f1e22640 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Thu, 1 May 2025 09:32:10 -0400 Subject: [PATCH 099/499] [PM-20032] Give option to skip token refresh on `fullSync` (#14423) * Give option to skip token refresh on fullSync * Fix listener --- .../sync/foreground-sync.service.spec.ts | 78 ++++++- .../platform/sync/foreground-sync.service.ts | 20 +- .../sync/sync-service.listener.spec.ts | 22 +- .../platform/sync/sync-service.listener.ts | 9 +- .../src/platform/sync/core-sync.service.ts | 3 + .../sync/default-sync.service.spec.ts | 199 ++++++++++++++++++ .../src/platform/sync/default-sync.service.ts | 15 +- libs/common/src/platform/sync/sync.service.ts | 29 ++- 8 files changed, 355 insertions(+), 20 deletions(-) create mode 100644 libs/common/src/platform/sync/default-sync.service.spec.ts diff --git a/apps/browser/src/platform/sync/foreground-sync.service.spec.ts b/apps/browser/src/platform/sync/foreground-sync.service.spec.ts index f5daff93815..34ee4fa0f77 100644 --- a/apps/browser/src/platform/sync/foreground-sync.service.spec.ts +++ b/apps/browser/src/platform/sync/foreground-sync.service.spec.ts @@ -8,6 +8,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { MessageListener, MessageSender } from "@bitwarden/common/platform/messaging"; import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { SyncOptions } from "@bitwarden/common/platform/sync/sync.service"; import { FakeStateProvider, mockAccountServiceWith } from "@bitwarden/common/spec"; import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction"; import { InternalSendService } from "@bitwarden/common/tools/send/services/send.service.abstraction"; @@ -80,7 +81,72 @@ describe("ForegroundSyncService", () => { const fullSyncPromise = sut.fullSync(true, false); expect(sut.syncInProgress).toBe(true); - const requestId = getAndAssertRequestId({ forceSync: true, allowThrowOnError: false }); + const requestId = getAndAssertRequestId({ + forceSync: true, + options: { allowThrowOnError: false, skipTokenRefresh: false }, + }); + + // Pretend the sync has finished + messages.next({ successfully: true, errorMessage: null, requestId: requestId }); + + const result = await fullSyncPromise; + + expect(sut.syncInProgress).toBe(false); + expect(result).toBe(true); + }); + + const testData: { + input: boolean | SyncOptions | undefined; + normalized: Required; + }[] = [ + { + input: undefined, + normalized: { allowThrowOnError: false, skipTokenRefresh: false }, + }, + { + input: true, + normalized: { allowThrowOnError: true, skipTokenRefresh: false }, + }, + { + input: false, + normalized: { allowThrowOnError: false, skipTokenRefresh: false }, + }, + { + input: { allowThrowOnError: false }, + normalized: { allowThrowOnError: false, skipTokenRefresh: false }, + }, + { + input: { allowThrowOnError: true }, + normalized: { allowThrowOnError: true, skipTokenRefresh: false }, + }, + { + input: { allowThrowOnError: false, skipTokenRefresh: false }, + normalized: { allowThrowOnError: false, skipTokenRefresh: false }, + }, + { + input: { allowThrowOnError: true, skipTokenRefresh: false }, + normalized: { allowThrowOnError: true, skipTokenRefresh: false }, + }, + { + input: { allowThrowOnError: true, skipTokenRefresh: true }, + normalized: { allowThrowOnError: true, skipTokenRefresh: true }, + }, + { + input: { allowThrowOnError: false, skipTokenRefresh: true }, + normalized: { allowThrowOnError: false, skipTokenRefresh: true }, + }, + ]; + + it.each(testData)("normalize input $input options correctly", async ({ input, normalized }) => { + const messages = new Subject(); + messageListener.messages$.mockReturnValue(messages); + const fullSyncPromise = sut.fullSync(true, input); + expect(sut.syncInProgress).toBe(true); + + const requestId = getAndAssertRequestId({ + forceSync: true, + options: normalized, + }); // Pretend the sync has finished messages.next({ successfully: true, errorMessage: null, requestId: requestId }); @@ -97,7 +163,10 @@ describe("ForegroundSyncService", () => { const fullSyncPromise = sut.fullSync(false, false); expect(sut.syncInProgress).toBe(true); - const requestId = getAndAssertRequestId({ forceSync: false, allowThrowOnError: false }); + const requestId = getAndAssertRequestId({ + forceSync: false, + options: { allowThrowOnError: false, skipTokenRefresh: false }, + }); // Pretend the sync has finished messages.next({ @@ -118,7 +187,10 @@ describe("ForegroundSyncService", () => { const fullSyncPromise = sut.fullSync(true, true); expect(sut.syncInProgress).toBe(true); - const requestId = getAndAssertRequestId({ forceSync: true, allowThrowOnError: true }); + const requestId = getAndAssertRequestId({ + forceSync: true, + options: { allowThrowOnError: true, skipTokenRefresh: false }, + }); // Pretend the sync has finished messages.next({ diff --git a/apps/browser/src/platform/sync/foreground-sync.service.ts b/apps/browser/src/platform/sync/foreground-sync.service.ts index a6ed7281851..ce776f53685 100644 --- a/apps/browser/src/platform/sync/foreground-sync.service.ts +++ b/apps/browser/src/platform/sync/foreground-sync.service.ts @@ -14,6 +14,7 @@ import { import { Utils } from "@bitwarden/common/platform/misc/utils"; import { StateProvider } from "@bitwarden/common/platform/state"; import { CoreSyncService } from "@bitwarden/common/platform/sync/internal"; +import { SyncOptions } from "@bitwarden/common/platform/sync/sync.service"; import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction"; import { InternalSendService } from "@bitwarden/common/tools/send/services/send.service.abstraction"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; @@ -22,7 +23,7 @@ import { InternalFolderService } from "@bitwarden/common/vault/abstractions/fold import { FULL_SYNC_FINISHED } from "./sync-service.listener"; -export type FullSyncMessage = { forceSync: boolean; allowThrowOnError: boolean; requestId: string }; +export type FullSyncMessage = { forceSync: boolean; options: SyncOptions; requestId: string }; export const DO_FULL_SYNC = new CommandDefinition("doFullSync"); @@ -60,9 +61,20 @@ export class ForegroundSyncService extends CoreSyncService { ); } - async fullSync(forceSync: boolean, allowThrowOnError: boolean = false): Promise { + async fullSync( + forceSync: boolean, + allowThrowOnErrorOrOptions?: boolean | SyncOptions, + ): Promise { this.syncInProgress = true; try { + // Normalize options + const options = + typeof allowThrowOnErrorOrOptions === "boolean" + ? { allowThrowOnError: allowThrowOnErrorOrOptions, skipTokenRefresh: false } + : { + allowThrowOnError: allowThrowOnErrorOrOptions?.allowThrowOnError ?? false, + skipTokenRefresh: allowThrowOnErrorOrOptions?.skipTokenRefresh ?? false, + }; const requestId = Utils.newGuid(); const syncCompletedPromise = firstValueFrom( this.messageListener.messages$(FULL_SYNC_FINISHED).pipe( @@ -79,10 +91,10 @@ export class ForegroundSyncService extends CoreSyncService { }), ), ); - this.messageSender.send(DO_FULL_SYNC, { forceSync, allowThrowOnError, requestId }); + this.messageSender.send(DO_FULL_SYNC, { forceSync, options, requestId }); const result = await syncCompletedPromise; - if (allowThrowOnError && result.errorMessage != null) { + if (options.allowThrowOnError && result.errorMessage != null) { throw new Error(result.errorMessage); } diff --git a/apps/browser/src/platform/sync/sync-service.listener.spec.ts b/apps/browser/src/platform/sync/sync-service.listener.spec.ts index 51f97e9f879..9682e2cdb57 100644 --- a/apps/browser/src/platform/sync/sync-service.listener.spec.ts +++ b/apps/browser/src/platform/sync/sync-service.listener.spec.ts @@ -27,11 +27,18 @@ describe("SyncServiceListener", () => { const emissionPromise = firstValueFrom(listener); syncService.fullSync.mockResolvedValueOnce(value); - messages.next({ forceSync: true, allowThrowOnError: false, requestId: "1" }); + messages.next({ + forceSync: true, + options: { allowThrowOnError: false, skipTokenRefresh: false }, + requestId: "1", + }); await emissionPromise; - expect(syncService.fullSync).toHaveBeenCalledWith(true, false); + expect(syncService.fullSync).toHaveBeenCalledWith(true, { + allowThrowOnError: false, + skipTokenRefresh: false, + }); expect(messageSender.send).toHaveBeenCalledWith(FULL_SYNC_FINISHED, { successfully: value, errorMessage: null, @@ -45,11 +52,18 @@ describe("SyncServiceListener", () => { const emissionPromise = firstValueFrom(listener); syncService.fullSync.mockRejectedValueOnce(new Error("SyncError")); - messages.next({ forceSync: true, allowThrowOnError: false, requestId: "1" }); + messages.next({ + forceSync: true, + options: { allowThrowOnError: false, skipTokenRefresh: false }, + requestId: "1", + }); await emissionPromise; - expect(syncService.fullSync).toHaveBeenCalledWith(true, false); + expect(syncService.fullSync).toHaveBeenCalledWith(true, { + allowThrowOnError: false, + skipTokenRefresh: false, + }); expect(messageSender.send).toHaveBeenCalledWith(FULL_SYNC_FINISHED, { successfully: false, errorMessage: "SyncError", diff --git a/apps/browser/src/platform/sync/sync-service.listener.ts b/apps/browser/src/platform/sync/sync-service.listener.ts index b7171528648..4274eafcf6a 100644 --- a/apps/browser/src/platform/sync/sync-service.listener.ts +++ b/apps/browser/src/platform/sync/sync-service.listener.ts @@ -9,6 +9,7 @@ import { MessageSender, isExternalMessage, } from "@bitwarden/common/platform/messaging"; +import { SyncOptions } from "@bitwarden/common/platform/sync/sync.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { DO_FULL_SYNC } from "./foreground-sync.service"; @@ -34,15 +35,15 @@ export class SyncServiceListener { listener$(): Observable { return this.messageListener.messages$(DO_FULL_SYNC).pipe( filter((message) => isExternalMessage(message)), - concatMap(async ({ forceSync, allowThrowOnError, requestId }) => { - await this.doFullSync(forceSync, allowThrowOnError, requestId); + concatMap(async ({ forceSync, options, requestId }) => { + await this.doFullSync(forceSync, options, requestId); }), ); } - private async doFullSync(forceSync: boolean, allowThrowOnError: boolean, requestId: string) { + private async doFullSync(forceSync: boolean, options: SyncOptions, requestId: string) { try { - const result = await this.syncService.fullSync(forceSync, allowThrowOnError); + const result = await this.syncService.fullSync(forceSync, options); this.messageSender.send(FULL_SYNC_FINISHED, { successfully: result, errorMessage: null, diff --git a/libs/common/src/platform/sync/core-sync.service.ts b/libs/common/src/platform/sync/core-sync.service.ts index 1865ffb852f..4020c75f764 100644 --- a/libs/common/src/platform/sync/core-sync.service.ts +++ b/libs/common/src/platform/sync/core-sync.service.ts @@ -28,6 +28,8 @@ import { StateService } from "../abstractions/state.service"; import { MessageSender } from "../messaging"; import { StateProvider, SYNC_DISK, UserKeyDefinition } from "../state"; +import { SyncOptions } from "./sync.service"; + const LAST_SYNC_DATE = new UserKeyDefinition(SYNC_DISK, "lastSync", { deserializer: (d) => (d != null ? new Date(d) : null), clearOn: ["logout"], @@ -55,6 +57,7 @@ export abstract class CoreSyncService implements SyncService { protected readonly stateProvider: StateProvider, ) {} + abstract fullSync(forceSync: boolean, syncOptions?: SyncOptions): Promise; abstract fullSync(forceSync: boolean, allowThrowOnError?: boolean): Promise; async getLastSync(): Promise { diff --git a/libs/common/src/platform/sync/default-sync.service.spec.ts b/libs/common/src/platform/sync/default-sync.service.spec.ts new file mode 100644 index 00000000000..ded06c8be6b --- /dev/null +++ b/libs/common/src/platform/sync/default-sync.service.spec.ts @@ -0,0 +1,199 @@ +import { mock, MockProxy } from "jest-mock-extended"; +import { of } from "rxjs"; + +import { CollectionService } from "@bitwarden/admin-console/common"; +import { + LogoutReason, + UserDecryptionOptions, + UserDecryptionOptionsServiceAbstraction, +} from "@bitwarden/auth/common"; +import { KeyService } from "@bitwarden/key-management"; + +import { Matrix } from "../../../spec/matrix"; +import { ApiService } from "../../abstractions/api.service"; +import { InternalOrganizationServiceAbstraction } from "../../admin-console/abstractions/organization/organization.service.abstraction"; +import { InternalPolicyService } from "../../admin-console/abstractions/policy/policy.service.abstraction"; +import { ProviderService } from "../../admin-console/abstractions/provider.service"; +import { Account, AccountService } from "../../auth/abstractions/account.service"; +import { AuthService } from "../../auth/abstractions/auth.service"; +import { AvatarService } from "../../auth/abstractions/avatar.service"; +import { TokenService } from "../../auth/abstractions/token.service"; +import { AuthenticationStatus } from "../../auth/enums/authentication-status"; +import { DomainSettingsService } from "../../autofill/services/domain-settings.service"; +import { BillingAccountProfileStateService } from "../../billing/abstractions"; +import { KeyConnectorService } from "../../key-management/key-connector/abstractions/key-connector.service"; +import { InternalMasterPasswordServiceAbstraction } from "../../key-management/master-password/abstractions/master-password.service.abstraction"; +import { SendApiService } from "../../tools/send/services/send-api.service.abstraction"; +import { InternalSendService } from "../../tools/send/services/send.service.abstraction"; +import { UserId } from "../../types/guid"; +import { CipherService } from "../../vault/abstractions/cipher.service"; +import { FolderApiServiceAbstraction } from "../../vault/abstractions/folder/folder-api.service.abstraction"; +import { InternalFolderService } from "../../vault/abstractions/folder/folder.service.abstraction"; +import { LogService } from "../abstractions/log.service"; +import { StateService } from "../abstractions/state.service"; +import { MessageSender } from "../messaging"; +import { StateProvider } from "../state"; + +import { DefaultSyncService } from "./default-sync.service"; +import { SyncResponse } from "./sync.response"; + +describe("DefaultSyncService", () => { + let masterPasswordAbstraction: MockProxy; + let accountService: MockProxy; + let apiService: MockProxy; + let domainSettingsService: MockProxy; + let folderService: MockProxy; + let cipherService: MockProxy; + let keyService: MockProxy; + let collectionService: MockProxy; + let messageSender: MockProxy; + let policyService: MockProxy; + let sendService: MockProxy; + let logService: MockProxy; + let keyConnectorService: MockProxy; + let stateService: MockProxy; + let providerService: MockProxy; + let folderApiService: MockProxy; + let organizationService: MockProxy; + let sendApiService: MockProxy; + let userDecryptionOptionsService: MockProxy; + let avatarService: MockProxy; + let logoutCallback: jest.Mock, [logoutReason: LogoutReason, userId?: UserId]>; + let billingAccountProfileStateService: MockProxy; + let tokenService: MockProxy; + let authService: MockProxy; + let stateProvider: MockProxy; + + let sut: DefaultSyncService; + + beforeEach(() => { + masterPasswordAbstraction = mock(); + accountService = mock(); + apiService = mock(); + domainSettingsService = mock(); + folderService = mock(); + cipherService = mock(); + keyService = mock(); + collectionService = mock(); + messageSender = mock(); + policyService = mock(); + sendService = mock(); + logService = mock(); + keyConnectorService = mock(); + stateService = mock(); + providerService = mock(); + folderApiService = mock(); + organizationService = mock(); + sendApiService = mock(); + userDecryptionOptionsService = mock(); + avatarService = mock(); + logoutCallback = jest.fn(); + billingAccountProfileStateService = mock(); + tokenService = mock(); + authService = mock(); + stateProvider = mock(); + + sut = new DefaultSyncService( + masterPasswordAbstraction, + accountService, + apiService, + domainSettingsService, + folderService, + cipherService, + keyService, + collectionService, + messageSender, + policyService, + sendService, + logService, + keyConnectorService, + stateService, + providerService, + folderApiService, + organizationService, + sendApiService, + userDecryptionOptionsService, + avatarService, + logoutCallback, + billingAccountProfileStateService, + tokenService, + authService, + stateProvider, + ); + }); + + const user1 = "user1" as UserId; + + describe("fullSync", () => { + beforeEach(() => { + accountService.activeAccount$ = of({ id: user1 } as Account); + Matrix.autoMockMethod(authService.authStatusFor$, () => of(AuthenticationStatus.Unlocked)); + apiService.getSync.mockResolvedValue( + new SyncResponse({ + profile: { + id: user1, + }, + folders: [], + collections: [], + ciphers: [], + sends: [], + domains: [], + policies: [], + }), + ); + Matrix.autoMockMethod(userDecryptionOptionsService.userDecryptionOptionsById$, () => + of({ hasMasterPassword: true } satisfies UserDecryptionOptions), + ); + stateProvider.getUser.mockReturnValue(mock()); + }); + + it("does a token refresh when option missing from options", async () => { + await sut.fullSync(true, { allowThrowOnError: false }); + + expect(apiService.refreshIdentityToken).toHaveBeenCalledTimes(1); + expect(apiService.getSync).toHaveBeenCalledTimes(1); + }); + + it("does a token refresh when boolean passed in", async () => { + await sut.fullSync(true, false); + + expect(apiService.refreshIdentityToken).toHaveBeenCalledTimes(1); + expect(apiService.getSync).toHaveBeenCalledTimes(1); + }); + + it("does a token refresh when skipTokenRefresh option passed in with false and allowThrowOnError also passed in", async () => { + await sut.fullSync(true, { allowThrowOnError: false, skipTokenRefresh: false }); + + expect(apiService.refreshIdentityToken).toHaveBeenCalledTimes(1); + expect(apiService.getSync).toHaveBeenCalledTimes(1); + }); + + it("does a token refresh when skipTokenRefresh option passed in with false by itself", async () => { + await sut.fullSync(true, { skipTokenRefresh: false }); + + expect(apiService.refreshIdentityToken).toHaveBeenCalledTimes(1); + expect(apiService.getSync).toHaveBeenCalledTimes(1); + }); + + it("does not do a token refresh when skipTokenRefresh passed in as true", async () => { + await sut.fullSync(true, { skipTokenRefresh: true }); + + expect(apiService.refreshIdentityToken).not.toHaveBeenCalled(); + expect(apiService.getSync).toHaveBeenCalledTimes(1); + }); + + it("does not do a token refresh when skipTokenRefresh passed in as true and allowThrowOnError also passed in", async () => { + await sut.fullSync(true, { allowThrowOnError: false, skipTokenRefresh: true }); + + expect(apiService.refreshIdentityToken).not.toHaveBeenCalled(); + expect(apiService.getSync).toHaveBeenCalledTimes(1); + }); + + it("does a token refresh when nothing passed in", async () => { + await sut.fullSync(true); + + expect(apiService.refreshIdentityToken).toHaveBeenCalledTimes(1); + expect(apiService.getSync).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/libs/common/src/platform/sync/default-sync.service.ts b/libs/common/src/platform/sync/default-sync.service.ts index a6b1b974645..faf54f11912 100644 --- a/libs/common/src/platform/sync/default-sync.service.ts +++ b/libs/common/src/platform/sync/default-sync.service.ts @@ -54,6 +54,7 @@ import { MessageSender } from "../messaging"; import { StateProvider } from "../state"; import { CoreSyncService } from "./core-sync.service"; +import { SyncOptions } from "./sync.service"; export class DefaultSyncService extends CoreSyncService { syncInProgress = false; @@ -102,7 +103,15 @@ export class DefaultSyncService extends CoreSyncService { ); } - override async fullSync(forceSync: boolean, allowThrowOnError = false): Promise { + override async fullSync( + forceSync: boolean, + allowThrowOnErrorOrOptions?: boolean | SyncOptions, + ): Promise { + const { allowThrowOnError = false, skipTokenRefresh = false } = + typeof allowThrowOnErrorOrOptions === "boolean" + ? { allowThrowOnError: allowThrowOnErrorOrOptions } + : (allowThrowOnErrorOrOptions ?? {}); + const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(map((a) => a?.id))); this.syncStarted(); const authStatus = await firstValueFrom(this.authService.authStatusFor$(userId)); @@ -127,7 +136,9 @@ export class DefaultSyncService extends CoreSyncService { } try { - await this.apiService.refreshIdentityToken(); + if (!skipTokenRefresh) { + await this.apiService.refreshIdentityToken(); + } const response = await this.apiService.getSync(); await this.syncProfile(response.profile); diff --git a/libs/common/src/platform/sync/sync.service.ts b/libs/common/src/platform/sync/sync.service.ts index 967e4db27a5..6ef62fc9cb8 100644 --- a/libs/common/src/platform/sync/sync.service.ts +++ b/libs/common/src/platform/sync/sync.service.ts @@ -7,6 +7,26 @@ import { } from "../../models/response/notification.response"; import { UserId } from "../../types/guid"; +/** + * A set of options for configuring how a {@link SyncService.fullSync} call should behave. + */ +export type SyncOptions = { + /** + * A boolean dictating whether or not caught errors should be rethrown. + * `true` if they can be rethrown, `false` if they should not be rethrown. + * @default false + */ + allowThrowOnError?: boolean; + /** + * A boolean dictating whether or not to do a token refresh before doing the sync. + * `true` if the refresh can be skipped, likely because one was done soon before the call to + * `fullSync`. `false` if the token refresh should be done before getting data. + * + * @default false + */ + skipTokenRefresh?: boolean; +}; + /** * A class encapsulating sync operations and data. */ @@ -47,9 +67,12 @@ export abstract class SyncService { * as long as the current user is authenticated. If `false` it will only sync if either a sync * has not happened before or the last sync date for the active user is before their account * revision date. Try to always use `false` if possible. - * - * @param allowThrowOnError A boolean dictating whether or not caught errors should be rethrown. - * `true` if they can be rethrown, `false` if they should not be rethrown. + * @param syncOptions Options for customizing how the sync call should behave. + */ + abstract fullSync(forceSync: boolean, syncOptions?: SyncOptions): Promise; + + /** + * @deprecated Use the overload taking {@link SyncOptions} instead. */ abstract fullSync(forceSync: boolean, allowThrowOnError?: boolean): Promise; From 8090586b52006078250397916e67b9ad32526181 Mon Sep 17 00:00:00 2001 From: Matt Bishop Date: Thu, 1 May 2025 07:18:09 -0700 Subject: [PATCH 100/499] Fix some references to master (#14578) * Fix some references to master * Fix broken links --- LICENSE.txt | 6 +++--- LICENSE_BITWARDEN.txt | 2 +- apps/browser/README.md | 4 ++-- apps/cli/README.md | 2 +- apps/desktop/README.md | 2 +- apps/web/README.md | 6 +++--- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/LICENSE.txt b/LICENSE.txt index 55bf3b3f736..8ad59f788b3 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -5,13 +5,13 @@ specifies another license. Bitwarden Licensed code is found only in the /bitwarden_license directory. GPL v3.0: -https://github.com/bitwarden/web/blob/master/LICENSE_GPL.txt +https://github.com/bitwarden/clients/blob/main/LICENSE_GPL.txt Bitwarden License v1.0: -https://github.com/bitwarden/web/blob/master/LICENSE_BITWARDEN.txt +https://github.com/bitwarden/clients/blob/main/LICENSE_BITWARDEN.txt No grant of any rights in the trademarks, service marks, or logos of Bitwarden is made (except as may be necessary to comply with the notice requirements as applicable), and use of any Bitwarden trademarks must comply with Bitwarden Trademark Guidelines -. +. diff --git a/LICENSE_BITWARDEN.txt b/LICENSE_BITWARDEN.txt index 08e09f28639..938946a09a1 100644 --- a/LICENSE_BITWARDEN.txt +++ b/LICENSE_BITWARDEN.txt @@ -56,7 +56,7 @@ such Open Source Software only. logos of any Contributor (except as may be necessary to comply with the notice requirements in Section 2.3), and use of any Bitwarden trademarks must comply with Bitwarden Trademark Guidelines -. +. 3. TERMINATION diff --git a/apps/browser/README.md b/apps/browser/README.md index c99d0844a09..fdeb1307567 100644 --- a/apps/browser/README.md +++ b/apps/browser/README.md @@ -1,4 +1,4 @@ -[![Github Workflow build browser on master](https://github.com/bitwarden/clients/actions/workflows/build-browser.yml/badge.svg?branch=master)](https://github.com/bitwarden/clients/actions/workflows/build-browser.yml?query=branch:master) +[![Github Workflow build browser on main](https://github.com/bitwarden/clients/actions/workflows/build-browser.yml/badge.svg?branch=main)](https://github.com/bitwarden/clients/actions/workflows/build-browser.yml?query=branch:main) [![Crowdin](https://d322cqt584bo4o.cloudfront.net/bitwarden-browser/localized.svg)](https://crowdin.com/project/bitwarden-browser) [![Join the chat at https://gitter.im/bitwarden/Lobby](https://badges.gitter.im/bitwarden/Lobby.svg)](https://gitter.im/bitwarden/Lobby) @@ -15,7 +15,7 @@ The Bitwarden browser extension is written using the Web Extension API and Angular. -![My Vault](https://raw.githubusercontent.com/bitwarden/brand/master/screenshots/browser-chrome.png) +![My Vault](https://raw.githubusercontent.com/bitwarden/brand/main/screenshots/web-browser-extension-generator.png) ## Documentation diff --git a/apps/cli/README.md b/apps/cli/README.md index d39c0e39c8f..2b13270cdba 100644 --- a/apps/cli/README.md +++ b/apps/cli/README.md @@ -1,4 +1,4 @@ -[![Github Workflow build on master](https://github.com/bitwarden/clients/actions/workflows/build-cli.yml/badge.svg?branch=master)](https://github.com/bitwarden/clients/actions/workflows/build-cli.yml?query=branch:master) +[![Github Workflow build on main](https://github.com/bitwarden/clients/actions/workflows/build-cli.yml/badge.svg?branch=main)](https://github.com/bitwarden/clients/actions/workflows/build-cli.yml?query=branch:main) [![Join the chat at https://gitter.im/bitwarden/Lobby](https://badges.gitter.im/bitwarden/Lobby.svg)](https://gitter.im/bitwarden/Lobby) # Bitwarden Command-line Interface diff --git a/apps/desktop/README.md b/apps/desktop/README.md index 6578699369b..ee13f451641 100644 --- a/apps/desktop/README.md +++ b/apps/desktop/README.md @@ -1,4 +1,4 @@ -[![Github Workflow build on master](https://github.com/bitwarden/clients/actions/workflows/build-desktop.yml/badge.svg?branch=master)](https://github.com/bitwarden/clients/actions/workflows/build-desktop.yml?query=branch:master) +[![Github Workflow build on main](https://github.com/bitwarden/clients/actions/workflows/build-desktop.yml/badge.svg?branch=main)](https://github.com/bitwarden/clients/actions/workflows/build-desktop.yml?query=branch:main) [![Crowdin](https://d322cqt584bo4o.cloudfront.net/bitwarden-desktop/localized.svg)](https://crowdin.com/project/bitwarden-desktop) [![Join the chat at https://gitter.im/bitwarden/Lobby](https://badges.gitter.im/bitwarden/Lobby.svg)](https://gitter.im/bitwarden/Lobby) diff --git a/apps/web/README.md b/apps/web/README.md index f43a9dc1614..c5e03eebb59 100644 --- a/apps/web/README.md +++ b/apps/web/README.md @@ -1,12 +1,12 @@

- +

The Bitwarden web project is an Angular application that powers the web vault (https://vault.bitwarden.com/).

- - Github Workflow build on master + + Github Workflow build on main Crowdin From 1b66f0f06b449444a0f81522c1fe8a28d998d88c Mon Sep 17 00:00:00 2001 From: Github Actions Date: Thu, 1 May 2025 14:22:26 +0000 Subject: [PATCH 101/499] Bumped Desktop client to 2025.5.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 e00df0b26df..21892cd1df8 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": "2025.4.2", + "version": "2025.5.0", "keywords": [ "bitwarden", "password", diff --git a/apps/desktop/src/package-lock.json b/apps/desktop/src/package-lock.json index f6449bd9626..b3a33dc75e3 100644 --- a/apps/desktop/src/package-lock.json +++ b/apps/desktop/src/package-lock.json @@ -1,12 +1,12 @@ { "name": "@bitwarden/desktop", - "version": "2025.4.2", + "version": "2025.5.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@bitwarden/desktop", - "version": "2025.4.2", + "version": "2025.5.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 45a6f6b90af..c180ed8c744 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": "2025.4.2", + "version": "2025.5.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 671951c0349..25322b844b7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -231,7 +231,7 @@ }, "apps/desktop": { "name": "@bitwarden/desktop", - "version": "2025.4.2", + "version": "2025.5.0", "hasInstallScript": true, "license": "GPL-3.0" }, From a7d04dc21276c5152ca6ab5d6f55159ec356c4c5 Mon Sep 17 00:00:00 2001 From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Date: Thu, 1 May 2025 16:36:00 +0100 Subject: [PATCH 102/499] [PM-17775] Allow admin to send f4 e sponsorship (#14390) * Added nav item for f4e in org admin console * shotgun surgery for adding "useAdminSponsoredFamilies" feature from the org table * Resolved issue with members nav item also being selected when f4e is selected * Separated out billing's logic from the org layout component * Removed unused observable * Moved logic to existing f4e policy service and added unit tests * Resolved script typescript error * Resolved goofy switchMap * Add changes for the issue orgs * Added changes for the dialog * Rename the files properly * Remove the commented code * Change the implement to align with design * Add todo comments * Remove the comment todo * Fix the uni test error * Resolve the unit test * Resolve the unit test issue * Resolve the pr comments on any and route * remove the any * remove the generic validator * Resolve the unit test * add validations for email * Add changes for the autoscale * Changes to allow admin to send F4E sponsorship * Fix the lint errors * Resolve the lint errors * Fix the revokeAccount message * Fix the lint runtime error * Resolve the lint issues * Remove unused components * Changes to add isadminInitiated * remove the FIXME comment * Resolve the failing test * Fix the pr comments * Resolve the orgkey and other comments * Resolve the lint error * Resolve the lint error * resolve the spelling error * refactor the getStatus method * Remove the deprecated method * Resolve the unusual type casting * revert the change --------- Co-authored-by: Conner Turnbull Co-authored-by: Conner Turnbull <133619638+cturnbull-bitwarden@users.noreply.github.com> --- .../add-sponsorship-dialog.component.html | 10 +- .../add-sponsorship-dialog.component.ts | 131 +++++---- .../free-bitwarden-families.component.html | 104 ++++++- .../free-bitwarden-families.component.ts | 261 +++++++++++++++--- ...rganization-member-families.component.html | 47 ---- .../organization-member-families.component.ts | 34 --- ...nization-sponsored-families.component.html | 87 ------ ...ganization-sponsored-families.component.ts | 39 --- .../sponsored-families.component.html | 2 +- .../sponsoring-org-row.component.html | 2 +- .../src/app/shared/loose-components.module.ts | 6 - apps/web/src/locales/en/messages.json | 21 +- .../src/services/jslib-services.module.ts | 7 + ...organization-sponsorship-create.request.ts | 1 + ...ion-sponsorship-api.service.abstraction.ts | 8 + ...ganization-sponsorship-invites.response.ts | 31 +++ .../organization-sponsorship-api.service.ts | 22 ++ 17 files changed, 491 insertions(+), 322 deletions(-) delete mode 100644 apps/web/src/app/billing/members/organization-member-families.component.html delete mode 100644 apps/web/src/app/billing/members/organization-member-families.component.ts delete mode 100644 apps/web/src/app/billing/members/organization-sponsored-families.component.html delete mode 100644 apps/web/src/app/billing/members/organization-sponsored-families.component.ts create mode 100644 libs/common/src/billing/abstractions/organizations/organization-sponsorship-api.service.abstraction.ts create mode 100644 libs/common/src/billing/models/response/organization-sponsorship-invites.response.ts create mode 100644 libs/common/src/billing/services/organization/organization-sponsorship-api.service.ts diff --git a/apps/web/src/app/billing/members/add-sponsorship-dialog.component.html b/apps/web/src/app/billing/members/add-sponsorship-dialog.component.html index 2dbcc577e54..405211d6ecb 100644 --- a/apps/web/src/app/billing/members/add-sponsorship-dialog.component.html +++ b/apps/web/src/app/billing/members/add-sponsorship-dialog.component.html @@ -34,7 +34,15 @@

- - - - - + + +

+ {{ "sponsorshipFreeBitwardenFamilies" | i18n }} +

+
+ {{ "sponsoredFamiliesInclude" | i18n }}: +
    +
  • {{ "sponsoredFamiliesPremiumAccess" | i18n }}
  • +
  • {{ "sponsoredFamiliesSharedCollectionsMessage" | i18n }}
  • +
+
- - - -
+

{{ "sponsoredBitwardenFamilies" | i18n }}

-

{{ "sponsoredFamiliesRemoveActiveSponsorship" | i18n }}

+ @if (loading()) { + + + {{ "loading" | i18n }} + + } + + @if (!loading() && sponsoredFamilies?.length > 0) { + + + + + {{ "recipient" | i18n }} + {{ "status" | i18n }} + {{ "notes" | i18n }} + + + + + @for (o of sponsoredFamilies; let i = $index; track i) { + + + {{ o.friendlyName }} + {{ o.statusMessage }} + {{ o.notes }} + + + + + +
+ + +
+ + +
+ } +
+
+
+
+ } @else if (!loading()) { +
+ Search +

{{ "noSponsoredFamiliesMessage" | i18n }}

+

{{ "nosponsoredFamiliesDetails" | i18n }}

+
+ } + + @if (!loading() && sponsoredFamilies.length > 0) { +

{{ "sponsoredFamiliesRemoveActiveSponsorship" | i18n }}

+ } +
+ diff --git a/apps/web/src/app/billing/members/free-bitwarden-families.component.ts b/apps/web/src/app/billing/members/free-bitwarden-families.component.ts index af43e5a4bc1..c141eaebd78 100644 --- a/apps/web/src/app/billing/members/free-bitwarden-families.component.ts +++ b/apps/web/src/app/billing/members/free-bitwarden-families.component.ts @@ -1,62 +1,259 @@ import { DialogRef } from "@angular/cdk/dialog"; -import { Component, OnInit } from "@angular/core"; -import { Router } from "@angular/router"; -import { firstValueFrom } from "rxjs"; +import { formatDate } from "@angular/common"; +import { Component, OnInit, signal } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { ActivatedRoute } from "@angular/router"; +import { firstValueFrom, map, Observable, switchMap } from "rxjs"; -import { DialogService } from "@bitwarden/components"; +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { OrganizationSponsorshipApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/organizations/organization-sponsorship-api.service.abstraction"; +import { OrganizationSponsorshipInvitesResponse } from "@bitwarden/common/billing/models/response/organization-sponsorship-invites.response"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; +import { StateProvider } from "@bitwarden/common/platform/state"; +import { OrganizationId, UserId } from "@bitwarden/common/types/guid"; +import { OrgKey } from "@bitwarden/common/types/key"; +import { DialogService, ToastService } from "@bitwarden/components"; +import { KeyService } from "@bitwarden/key-management"; -import { FreeFamiliesPolicyService } from "../services/free-families-policy.service"; - -import { - AddSponsorshipDialogComponent, - AddSponsorshipDialogResult, -} from "./add-sponsorship-dialog.component"; -import { SponsoredFamily } from "./types/sponsored-family"; +import { AddSponsorshipDialogComponent } from "./add-sponsorship-dialog.component"; @Component({ selector: "app-free-bitwarden-families", templateUrl: "free-bitwarden-families.component.html", }) export class FreeBitwardenFamiliesComponent implements OnInit { + loading = signal(true); tabIndex = 0; - sponsoredFamilies: SponsoredFamily[] = []; + sponsoredFamilies: OrganizationSponsorshipInvitesResponse[] = []; + + organizationId = ""; + organizationKey$: Observable; + + private locale: string = ""; constructor( - private router: Router, + private route: ActivatedRoute, private dialogService: DialogService, - private freeFamiliesPolicyService: FreeFamiliesPolicyService, - ) {} + private apiService: ApiService, + private encryptService: EncryptService, + private keyService: KeyService, + private platformUtilsService: PlatformUtilsService, + private i18nService: I18nService, + private logService: LogService, + private toastService: ToastService, + private organizationSponsorshipApiService: OrganizationSponsorshipApiServiceAbstraction, + private stateProvider: StateProvider, + ) { + this.organizationId = this.route.snapshot.params.organizationId || ""; + this.organizationKey$ = this.stateProvider.activeUserId$.pipe( + switchMap( + (userId) => + this.keyService.orgKeys$(userId as UserId) as Observable>, + ), + map((organizationKeysById) => organizationKeysById[this.organizationId as OrganizationId]), + takeUntilDestroyed(), + ); + } async ngOnInit() { - await this.preventAccessToFreeFamiliesPage(); + this.locale = await firstValueFrom(this.i18nService.locale$); + await this.loadSponsorships(); + + this.loading.set(false); + } + + async loadSponsorships() { + if (!this.organizationId) { + return; + } + + const [response, orgKey] = await Promise.all([ + this.organizationSponsorshipApiService.getOrganizationSponsorship(this.organizationId), + firstValueFrom(this.organizationKey$), + ]); + + if (!orgKey) { + this.logService.error("Organization key not found"); + return; + } + + const organizationFamilies = response.data; + + this.sponsoredFamilies = await Promise.all( + organizationFamilies.map(async (family) => { + let decryptedNote = ""; + try { + decryptedNote = await this.encryptService.decryptString( + new EncString(family.notes), + orgKey, + ); + } catch (e) { + this.logService.error(e); + } + + const { statusMessage, statusClass } = this.getStatus( + this.isSelfHosted, + family.toDelete, + family.validUntil, + family.lastSyncDate, + this.locale, + ); + + const newFamily = { + ...family, + notes: decryptedNote, + statusMessage: statusMessage || "", + statusClass: statusClass || "tw-text-success", + status: statusMessage || "", + }; + + return new OrganizationSponsorshipInvitesResponse(newFamily); + }), + ); } async addSponsorship() { - const addSponsorshipDialogRef: DialogRef = - AddSponsorshipDialogComponent.open(this.dialogService); + const addSponsorshipDialogRef: DialogRef = AddSponsorshipDialogComponent.open( + this.dialogService, + { + data: { + organizationId: this.organizationId, + organizationKey: await firstValueFrom(this.organizationKey$), + }, + }, + ); - const dialogRef = await firstValueFrom(addSponsorshipDialogRef.closed); + await firstValueFrom(addSponsorshipDialogRef.closed); - if (dialogRef?.value) { - this.sponsoredFamilies = [dialogRef.value, ...this.sponsoredFamilies]; + await this.loadSponsorships(); + } + + async removeSponsorship(sponsorship: OrganizationSponsorshipInvitesResponse) { + try { + await this.doRevokeSponsorship(sponsorship); + } catch (e) { + this.logService.error(e); } } - removeSponsorhip(sponsorship: any) { - const index = this.sponsoredFamilies.findIndex( - (e) => e.sponsorshipEmail == sponsorship.sponsorshipEmail, - ); - this.sponsoredFamilies.splice(index, 1); + get isSelfHosted(): boolean { + return this.platformUtilsService.isSelfHost(); } - private async preventAccessToFreeFamiliesPage() { - const showFreeFamiliesPage = await firstValueFrom( - this.freeFamiliesPolicyService.showFreeFamilies$, - ); + async resendEmail(sponsorship: OrganizationSponsorshipInvitesResponse) { + await this.apiService.postResendSponsorshipOffer(sponsorship.sponsoringOrganizationUserId); + this.toastService.showToast({ + variant: "success", + title: undefined, + message: this.i18nService.t("emailSent"), + }); + } - if (!showFreeFamiliesPage) { - await this.router.navigate(["/"]); + private async doRevokeSponsorship(sponsorship: OrganizationSponsorshipInvitesResponse) { + const content = sponsorship.validUntil + ? this.i18nService.t( + "updatedRevokeSponsorshipConfirmationForAcceptedSponsorship", + sponsorship.friendlyName, + formatDate(sponsorship.validUntil, "MM/dd/yyyy", this.locale), + ) + : this.i18nService.t( + "updatedRevokeSponsorshipConfirmationForSentSponsorship", + sponsorship.friendlyName, + ); + + const confirmed = await this.dialogService.openSimpleDialog({ + title: `${this.i18nService.t("removeSponsorship")}?`, + content, + acceptButtonText: { key: "remove" }, + type: "warning", + }); + + if (!confirmed) { return; } + + await this.apiService.deleteRevokeSponsorship(sponsorship.sponsoringOrganizationUserId); + + this.toastService.showToast({ + variant: "success", + title: undefined, + message: this.i18nService.t("reclaimedFreePlan"), + }); + + await this.loadSponsorships(); + } + + private getStatus( + selfHosted: boolean, + toDelete?: boolean, + validUntil?: Date, + lastSyncDate?: Date, + locale: string = "", + ): { statusMessage: string; statusClass: "tw-text-success" | "tw-text-danger" } { + /* + * Possible Statuses: + * Requested (self-hosted only) + * Sent + * Active + * RequestRevoke + * RevokeWhenExpired + */ + + if (toDelete && validUntil) { + // They want to delete but there is a valid until date which means there is an active sponsorship + return { + statusMessage: this.i18nService.t( + "revokeWhenExpired", + formatDate(validUntil, "MM/dd/yyyy", locale), + ), + statusClass: "tw-text-danger", + }; + } + + if (toDelete) { + // They want to delete and we don't have a valid until date so we can + // this should only happen on a self-hosted install + return { + statusMessage: this.i18nService.t("requestRemoved"), + statusClass: "tw-text-danger", + }; + } + + if (validUntil) { + // They don't want to delete and they have a valid until date + // that means they are actively sponsoring someone + return { + statusMessage: this.i18nService.t("active"), + statusClass: "tw-text-success", + }; + } + + if (selfHosted && lastSyncDate) { + // We are on a self-hosted install and it has been synced but we have not gotten + // a valid until date so we can't know if they are actively sponsoring someone + return { + statusMessage: this.i18nService.t("sent"), + statusClass: "tw-text-success", + }; + } + + if (!selfHosted) { + // We are in cloud and all other status checks have been false therefore we have + // sent the request but it hasn't been accepted yet + return { + statusMessage: this.i18nService.t("sent"), + statusClass: "tw-text-success", + }; + } + + // We are on a self-hosted install and we have not synced yet + return { + statusMessage: this.i18nService.t("requested"), + statusClass: "tw-text-success", + }; } } diff --git a/apps/web/src/app/billing/members/organization-member-families.component.html b/apps/web/src/app/billing/members/organization-member-families.component.html deleted file mode 100644 index c5b7283d9d9..00000000000 --- a/apps/web/src/app/billing/members/organization-member-families.component.html +++ /dev/null @@ -1,47 +0,0 @@ - - -

- {{ "membersWithSponsoredFamilies" | i18n }} -

- -

{{ "memberFamilies" | i18n }}

- - @if (loading) { - - - {{ "loading" | i18n }} - - } - - @if (!loading && memberFamilies?.length > 0) { - - - - - {{ "member" | i18n }} - {{ "status" | i18n }} - - - - - @for (o of memberFamilies; let i = $index; track i) { - - - {{ o.sponsorshipEmail }} - {{ o.status }} - - - } - - -
-
- } @else { -
- Search -

{{ "noMemberFamilies" | i18n }}

-

{{ "noMemberFamiliesDescription" | i18n }}

-
- } -
-
diff --git a/apps/web/src/app/billing/members/organization-member-families.component.ts b/apps/web/src/app/billing/members/organization-member-families.component.ts deleted file mode 100644 index 52c95646a11..00000000000 --- a/apps/web/src/app/billing/members/organization-member-families.component.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { Component, Input, OnDestroy, OnInit } from "@angular/core"; -import { Subject } from "rxjs"; - -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; - -import { SponsoredFamily } from "./types/sponsored-family"; - -@Component({ - selector: "app-organization-member-families", - templateUrl: "organization-member-families.component.html", -}) -export class OrganizationMemberFamiliesComponent implements OnInit, OnDestroy { - tabIndex = 0; - loading = false; - - @Input() memberFamilies: SponsoredFamily[] = []; - - private _destroy = new Subject(); - - constructor(private platformUtilsService: PlatformUtilsService) {} - - async ngOnInit() { - this.loading = false; - } - - ngOnDestroy(): void { - this._destroy.next(); - this._destroy.complete(); - } - - get isSelfHosted(): boolean { - return this.platformUtilsService.isSelfHost(); - } -} diff --git a/apps/web/src/app/billing/members/organization-sponsored-families.component.html b/apps/web/src/app/billing/members/organization-sponsored-families.component.html deleted file mode 100644 index 7db96deb4ab..00000000000 --- a/apps/web/src/app/billing/members/organization-sponsored-families.component.html +++ /dev/null @@ -1,87 +0,0 @@ - - -

- {{ "sponsorFreeBitwardenFamilies" | i18n }} -

-
- {{ "sponsoredFamiliesInclude" | i18n }}: -
    -
  • {{ "sponsoredFamiliesPremiumAccess" | i18n }}
  • -
  • {{ "sponsoredFamiliesSharedCollections" | i18n }}
  • -
-
- -

{{ "sponsoredBitwardenFamilies" | i18n }}

- - @if (loading) { - - - {{ "loading" | i18n }} - - } - - @if (!loading && sponsoredFamilies?.length > 0) { - - - - - {{ "recipient" | i18n }} - {{ "status" | i18n }} - {{ "notes" | i18n }} - - - - - @for (o of sponsoredFamilies; let i = $index; track i) { - - - {{ o.sponsorshipEmail }} - {{ o.status }} - {{ o.sponsorshipNote }} - - - - - -
- - -
- - -
- } -
-
-
-
- } @else { -
- Search -

{{ "noSponsoredFamilies" | i18n }}

-

{{ "noSponsoredFamiliesDescription" | i18n }}

-
- } -
-
diff --git a/apps/web/src/app/billing/members/organization-sponsored-families.component.ts b/apps/web/src/app/billing/members/organization-sponsored-families.component.ts deleted file mode 100644 index 7cc46634a38..00000000000 --- a/apps/web/src/app/billing/members/organization-sponsored-families.component.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core"; -import { Subject } from "rxjs"; - -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; - -import { SponsoredFamily } from "./types/sponsored-family"; - -@Component({ - selector: "app-organization-sponsored-families", - templateUrl: "organization-sponsored-families.component.html", -}) -export class OrganizationSponsoredFamiliesComponent implements OnInit, OnDestroy { - loading = false; - tabIndex = 0; - - @Input() sponsoredFamilies: SponsoredFamily[] = []; - @Output() removeSponsorshipEvent = new EventEmitter(); - - private _destroy = new Subject(); - - constructor(private platformUtilsService: PlatformUtilsService) {} - - async ngOnInit() { - this.loading = false; - } - - get isSelfHosted(): boolean { - return this.platformUtilsService.isSelfHost(); - } - - remove(sponsorship: SponsoredFamily) { - this.removeSponsorshipEvent.emit(sponsorship); - } - - ngOnDestroy(): void { - this._destroy.next(); - this._destroy.complete(); - } -} diff --git a/apps/web/src/app/billing/settings/sponsored-families.component.html b/apps/web/src/app/billing/settings/sponsored-families.component.html index 12e942aaf18..8e829ae70ef 100644 --- a/apps/web/src/app/billing/settings/sponsored-families.component.html +++ b/apps/web/src/app/billing/settings/sponsored-families.component.html @@ -13,7 +13,7 @@ {{ "sponsoredFamiliesInclude" | i18n }}:
  • {{ "sponsoredFamiliesPremiumAccess" | i18n }}
  • -
  • {{ "sponsoredFamiliesSharedCollections" | i18n }}
  • +
  • {{ "sponsoredFamiliesSharedCollectionsMessage" | i18n }}
diff --git a/apps/web/src/app/billing/settings/sponsoring-org-row.component.html b/apps/web/src/app/billing/settings/sponsoring-org-row.component.html index eeeaa256049..1e5690cd85a 100644 --- a/apps/web/src/app/billing/settings/sponsoring-org-row.component.html +++ b/apps/web/src/app/billing/settings/sponsoring-org-row.component.html @@ -32,7 +32,7 @@ type="button" bitMenuItem (click)="revokeSponsorship()" - [attr.aria-label]="'revokeAccount' | i18n: sponsoringOrg.familySponsorshipFriendlyName" + [attr.aria-label]="'revokeAccountMessage' | i18n: sponsoringOrg.familySponsorshipFriendlyName" > {{ "remove" | i18n }} diff --git a/apps/web/src/app/shared/loose-components.module.ts b/apps/web/src/app/shared/loose-components.module.ts index bec06888c57..90e4c6ba9c3 100644 --- a/apps/web/src/app/shared/loose-components.module.ts +++ b/apps/web/src/app/shared/loose-components.module.ts @@ -62,8 +62,6 @@ import { PipesModule } from "../vault/individual-vault/pipes/pipes.module"; import { PurgeVaultComponent } from "../vault/settings/purge-vault.component"; import { FreeBitwardenFamiliesComponent } from "./../billing/members/free-bitwarden-families.component"; -import { OrganizationMemberFamiliesComponent } from "./../billing/members/organization-member-families.component"; -import { OrganizationSponsoredFamiliesComponent } from "./../billing/members/organization-sponsored-families.component"; import { EnvironmentSelectorModule } from "./../components/environment-selector/environment-selector.module"; import { AccountFingerprintComponent } from "./components/account-fingerprint/account-fingerprint.component"; import { SharedModule } from "./shared.module"; @@ -128,8 +126,6 @@ import { SharedModule } from "./shared.module"; SelectableAvatarComponent, SetPasswordComponent, SponsoredFamiliesComponent, - OrganizationSponsoredFamiliesComponent, - OrganizationMemberFamiliesComponent, FreeBitwardenFamiliesComponent, SponsoringOrgRowComponent, UpdatePasswordComponent, @@ -176,8 +172,6 @@ import { SharedModule } from "./shared.module"; SelectableAvatarComponent, SetPasswordComponent, SponsoredFamiliesComponent, - OrganizationSponsoredFamiliesComponent, - OrganizationMemberFamiliesComponent, FreeBitwardenFamiliesComponent, SponsoringOrgRowComponent, UpdateTempPasswordComponent, diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 4650ded54bb..c81954ef9ca 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -6303,13 +6303,13 @@ "sponsoredBitwardenFamilies": { "message": "Sponsored families" }, - "noSponsoredFamilies": { + "noSponsoredFamiliesMessage": { "message": "No sponsored families" }, - "noSponsoredFamiliesDescription": { + "nosponsoredFamiliesDetails": { "message": "Sponsored non-member families plans will display here" }, - "sponsorFreeBitwardenFamilies": { + "sponsorshipFreeBitwardenFamilies": { "message": "Members of your organization are eligible for Free Bitwarden Families. You can sponsor Free Bitwarden Families for employees who are not a member of your Bitwarden organization. Sponsoring a non-member requires an available seat within your organization." }, "sponsoredFamiliesRemoveActiveSponsorship": { @@ -6327,8 +6327,8 @@ "sponsoredFamiliesPremiumAccess": { "message": "Premium access for up to 6 users" }, - "sponsoredFamiliesSharedCollections": { - "message": "Shared collections for Family secrets" + "sponsoredFamiliesSharedCollectionsMessage": { + "message": "Shared collections for family members" }, "memberFamilies": { "message": "Member families" @@ -6342,6 +6342,15 @@ "membersWithSponsoredFamilies": { "message": "Members of your organization are eligible for Free Bitwarden Families. Here you can see members who have sponsored a Families organization." }, + "organizationHasMemberMessage": { + "message": "A sponsorship cannot be sent to $EMAIL$ because they are a member of your organization.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, "badToken": { "message": "The link is no longer valid. Please have the sponsor resend the offer." }, @@ -6393,7 +6402,7 @@ "redeemedAccount": { "message": "Account redeemed" }, - "revokeAccount": { + "revokeAccountMessage": { "message": "Revoke account $NAME$", "placeholders": { "name": { diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 1cc2b591412..0d59f4a6547 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -136,11 +136,13 @@ import { import { AccountBillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/account/account-billing-api.service.abstraction"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { OrganizationBillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/organizations/organization-billing-api.service.abstraction"; +import { OrganizationSponsorshipApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/organizations/organization-sponsorship-api.service.abstraction"; import { TaxServiceAbstraction } from "@bitwarden/common/billing/abstractions/tax.service.abstraction"; import { AccountBillingApiService } from "@bitwarden/common/billing/services/account/account-billing-api.service"; import { DefaultBillingAccountProfileStateService } from "@bitwarden/common/billing/services/account/billing-account-profile-state.service"; import { BillingApiService } from "@bitwarden/common/billing/services/billing-api.service"; import { OrganizationBillingApiService } from "@bitwarden/common/billing/services/organization/organization-billing-api.service"; +import { OrganizationSponsorshipApiService } from "@bitwarden/common/billing/services/organization/organization-sponsorship-api.service"; import { OrganizationBillingService } from "@bitwarden/common/billing/services/organization-billing.service"; import { TaxService } from "@bitwarden/common/billing/services/tax.service"; import { BulkEncryptService } from "@bitwarden/common/key-management/crypto/abstractions/bulk-encrypt.service"; @@ -1063,6 +1065,11 @@ const safeProviders: SafeProvider[] = [ // subscribes to sync notifications and will update itself based on that. deps: [ApiServiceAbstraction, SyncService], }), + safeProvider({ + provide: OrganizationSponsorshipApiServiceAbstraction, + useClass: OrganizationSponsorshipApiService, + deps: [ApiServiceAbstraction], + }), safeProvider({ provide: OrganizationBillingApiServiceAbstraction, useClass: OrganizationBillingApiService, diff --git a/libs/common/src/admin-console/models/request/organization/organization-sponsorship-create.request.ts b/libs/common/src/admin-console/models/request/organization/organization-sponsorship-create.request.ts index 19e993487c2..726bd6a85e1 100644 --- a/libs/common/src/admin-console/models/request/organization/organization-sponsorship-create.request.ts +++ b/libs/common/src/admin-console/models/request/organization/organization-sponsorship-create.request.ts @@ -6,5 +6,6 @@ export class OrganizationSponsorshipCreateRequest { sponsoredEmail: string; planSponsorshipType: PlanSponsorshipType; friendlyName: string; + isAdminInitiated?: boolean; notes?: string; } diff --git a/libs/common/src/billing/abstractions/organizations/organization-sponsorship-api.service.abstraction.ts b/libs/common/src/billing/abstractions/organizations/organization-sponsorship-api.service.abstraction.ts new file mode 100644 index 00000000000..e6e395c69df --- /dev/null +++ b/libs/common/src/billing/abstractions/organizations/organization-sponsorship-api.service.abstraction.ts @@ -0,0 +1,8 @@ +import { ListResponse } from "../../../models/response/list.response"; +import { OrganizationSponsorshipInvitesResponse } from "../../models/response/organization-sponsorship-invites.response"; + +export abstract class OrganizationSponsorshipApiServiceAbstraction { + abstract getOrganizationSponsorship( + sponsoredOrgId: string, + ): Promise>; +} diff --git a/libs/common/src/billing/models/response/organization-sponsorship-invites.response.ts b/libs/common/src/billing/models/response/organization-sponsorship-invites.response.ts new file mode 100644 index 00000000000..87a2cae4699 --- /dev/null +++ b/libs/common/src/billing/models/response/organization-sponsorship-invites.response.ts @@ -0,0 +1,31 @@ +import { BaseResponse } from "../../../models/response/base.response"; +import { PlanSponsorshipType } from "../../enums"; + +export class OrganizationSponsorshipInvitesResponse extends BaseResponse { + sponsoringOrganizationUserId: string; + friendlyName: string; + offeredToEmail: string; + planSponsorshipType: PlanSponsorshipType; + lastSyncDate?: Date; + validUntil?: Date; + toDelete = false; + isAdminInitiated: boolean; + notes: string; + statusMessage?: string; + statusClass?: string; + + constructor(response: any) { + super(response); + this.sponsoringOrganizationUserId = this.getResponseProperty("SponsoringOrganizationUserId"); + this.friendlyName = this.getResponseProperty("FriendlyName"); + this.offeredToEmail = this.getResponseProperty("OfferedToEmail"); + this.planSponsorshipType = this.getResponseProperty("PlanSponsorshipType"); + this.lastSyncDate = this.getResponseProperty("LastSyncDate"); + this.validUntil = this.getResponseProperty("ValidUntil"); + this.toDelete = this.getResponseProperty("ToDelete") ?? false; + this.isAdminInitiated = this.getResponseProperty("IsAdminInitiated"); + this.notes = this.getResponseProperty("Notes"); + this.statusMessage = this.getResponseProperty("StatusMessage"); + this.statusClass = this.getResponseProperty("StatusClass"); + } +} diff --git a/libs/common/src/billing/services/organization/organization-sponsorship-api.service.ts b/libs/common/src/billing/services/organization/organization-sponsorship-api.service.ts new file mode 100644 index 00000000000..bb420377439 --- /dev/null +++ b/libs/common/src/billing/services/organization/organization-sponsorship-api.service.ts @@ -0,0 +1,22 @@ +import { ApiService } from "../../../abstractions/api.service"; +import { ListResponse } from "../../../models/response/list.response"; +import { OrganizationSponsorshipApiServiceAbstraction } from "../../abstractions/organizations/organization-sponsorship-api.service.abstraction"; +import { OrganizationSponsorshipInvitesResponse } from "../../models/response/organization-sponsorship-invites.response"; + +export class OrganizationSponsorshipApiService + implements OrganizationSponsorshipApiServiceAbstraction +{ + constructor(private apiService: ApiService) {} + async getOrganizationSponsorship( + sponsoredOrgId: string, + ): Promise> { + const r = await this.apiService.send( + "GET", + "/organization/sponsorship/" + sponsoredOrgId + "/sponsored", + null, + true, + true, + ); + return new ListResponse(r, OrganizationSponsorshipInvitesResponse); + } +} From c9dcba2506b1d811cfcfd91b58f0d794b7b570a4 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 1 May 2025 09:00:40 -0700 Subject: [PATCH 103/499] [deps]: Update docker/setup-qemu-action action to v3.6.0 (#14504) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build-web.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-web.yml b/.github/workflows/build-web.yml index 4ca6dc25aab..630e1e55682 100644 --- a/.github/workflows/build-web.yml +++ b/.github/workflows/build-web.yml @@ -196,7 +196,7 @@ jobs: } - name: Set up QEMU emulators - uses: docker/setup-qemu-action@68827325e0b33c7199eb31dd4e31fbe9023e06e3 # v3.0.0 + uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0 - name: Set up Docker Buildx uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0 From a50b45c505bd839d965856429543b5310c2f647b Mon Sep 17 00:00:00 2001 From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Date: Thu, 1 May 2025 17:12:28 +0100 Subject: [PATCH 104/499] Resolve the typo (#14584) --- .../billing/members/free-bitwarden-families.component.html | 2 +- .../app/billing/settings/sponsored-families.component.html | 2 +- apps/web/src/locales/en/messages.json | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/web/src/app/billing/members/free-bitwarden-families.component.html b/apps/web/src/app/billing/members/free-bitwarden-families.component.html index a156eb608d5..9e32fb925a8 100644 --- a/apps/web/src/app/billing/members/free-bitwarden-families.component.html +++ b/apps/web/src/app/billing/members/free-bitwarden-families.component.html @@ -11,7 +11,7 @@ {{ "sponsorshipFreeBitwardenFamilies" | i18n }}

- {{ "sponsoredFamiliesInclude" | i18n }}: + {{ "sponsoredFamiliesIncludeMessage" | i18n }}:
  • {{ "sponsoredFamiliesPremiumAccess" | i18n }}
  • {{ "sponsoredFamiliesSharedCollectionsMessage" | i18n }}
  • diff --git a/apps/web/src/app/billing/settings/sponsored-families.component.html b/apps/web/src/app/billing/settings/sponsored-families.component.html index 8e829ae70ef..5a6957718a3 100644 --- a/apps/web/src/app/billing/settings/sponsored-families.component.html +++ b/apps/web/src/app/billing/settings/sponsored-families.component.html @@ -10,7 +10,7 @@ {{ "sponsoredFamiliesEligible" | i18n }}

    - {{ "sponsoredFamiliesInclude" | i18n }}: + {{ "sponsoredFamiliesIncludeMessage" | i18n }}:
    • {{ "sponsoredFamiliesPremiumAccess" | i18n }}
    • {{ "sponsoredFamiliesSharedCollectionsMessage" | i18n }}
    • diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index c81954ef9ca..da71d1cc219 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -6321,8 +6321,8 @@ "sponsoredFamiliesEligibleCard": { "message": "Redeem your Free Bitwarden for Families plan today to keep your data secure even when you are not at work." }, - "sponsoredFamiliesInclude": { - "message": "The Bitwarden for Families plan include" + "sponsoredFamiliesIncludeMessage": { + "message": "The Bitwarden for Families plan includes" }, "sponsoredFamiliesPremiumAccess": { "message": "Premium access for up to 6 users" From 64daf5a889aac740bc626d9435ba6e692902e96d Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Thu, 1 May 2025 12:12:37 -0400 Subject: [PATCH 105/499] Require provider payment method during setup behind FF (#14550) --- .../shared/payment/payment.component.html | 2 +- .../shared/payment/payment.component.ts | 12 ++++ apps/web/src/locales/en/messages.json | 3 + .../providers/providers.module.ts | 2 + .../providers/setup/setup.component.html | 57 ++++++++++++++----- .../providers/setup/setup.component.ts | 17 +++++- .../provider/provider-setup.request.ts | 2 + libs/common/src/enums/feature-flag.enum.ts | 2 + 8 files changed, 80 insertions(+), 17 deletions(-) diff --git a/apps/web/src/app/billing/shared/payment/payment.component.html b/apps/web/src/app/billing/shared/payment/payment.component.html index c86975cd0e8..0d76d98e334 100644 --- a/apps/web/src/app/billing/shared/payment/payment.component.html +++ b/apps/web/src/app/billing/shared/payment/payment.component.html @@ -81,7 +81,7 @@ - {{ "verifyBankAccountWithStatementDescriptorWarning" | i18n }} + {{ bankAccountWarning }}
      diff --git a/apps/web/src/app/billing/shared/payment/payment.component.ts b/apps/web/src/app/billing/shared/payment/payment.component.ts index c7c3e31c89f..5911e377869 100644 --- a/apps/web/src/app/billing/shared/payment/payment.component.ts +++ b/apps/web/src/app/billing/shared/payment/payment.component.ts @@ -8,6 +8,7 @@ import { takeUntil } from "rxjs/operators"; import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions"; import { PaymentMethodType } from "@bitwarden/common/billing/enums"; import { TokenizedPaymentSourceRequest } from "@bitwarden/common/billing/models/request/tokenized-payment-source.request"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { SharedModule } from "../../../shared"; import { BillingServicesModule, BraintreeService, StripeService } from "../../services"; @@ -37,6 +38,8 @@ export class PaymentComponent implements OnInit, OnDestroy { /** If provided, will be invoked with the tokenized payment source during form submission. */ @Input() protected onSubmit?: (request: TokenizedPaymentSourceRequest) => Promise; + @Input() private bankAccountWarningOverride?: string; + @Output() submitted = new EventEmitter(); private destroy$ = new Subject(); @@ -56,6 +59,7 @@ export class PaymentComponent implements OnInit, OnDestroy { constructor( private billingApiService: BillingApiServiceAbstraction, private braintreeService: BraintreeService, + private i18nService: I18nService, private stripeService: StripeService, ) {} @@ -200,4 +204,12 @@ export class PaymentComponent implements OnInit, OnDestroy { private get usingStripe(): boolean { return this.usingBankAccount || this.usingCard; } + + get bankAccountWarning(): string { + if (this.bankAccountWarningOverride) { + return this.bankAccountWarningOverride; + } else { + return this.i18nService.t("verifyBankAccountWithStatementDescriptorWarning"); + } + } } diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index da71d1cc219..ce45b538bbe 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -10631,5 +10631,8 @@ }, "restart": { "message": "Restart" + }, + "verifyProviderBankAccountWithStatementDescriptorWarning": { + "message": "Payment with a bank account is only available to customers in the United States. You will be required to verify your bank account. We will make a micro-deposit within the next 1-2 business days. Enter the statement descriptor code from this deposit on the provider's subscription page to verify the bank account. Failure to verify the bank account will result in a missed payment and your subscription being suspended." } } diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/providers.module.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/providers.module.ts index dd9baa99948..1c15812edc8 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/providers.module.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/providers.module.ts @@ -7,6 +7,7 @@ import { JslibModule } from "@bitwarden/angular/jslib.module"; import { CardComponent, SearchModule } from "@bitwarden/components"; import { DangerZoneComponent } from "@bitwarden/web-vault/app/auth/settings/account/danger-zone.component"; import { OrganizationPlansComponent } from "@bitwarden/web-vault/app/billing"; +import { PaymentComponent } from "@bitwarden/web-vault/app/billing/shared/payment/payment.component"; import { VerifyBankAccountComponent } from "@bitwarden/web-vault/app/billing/shared/verify-bank-account/verify-bank-account.component"; import { OssModule } from "@bitwarden/web-vault/app/oss.module"; @@ -53,6 +54,7 @@ import { VerifyRecoverDeleteProviderComponent } from "./verify-recover-delete-pr ScrollingModule, VerifyBankAccountComponent, CardComponent, + PaymentComponent, ], declarations: [ AcceptProviderComponent, diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.html b/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.html index 38b4c3bc9de..4c5a35ea58d 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.html +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.html @@ -12,23 +12,50 @@

      {{ "setupProviderDesc" | i18n }}

      -

      {{ "generalInformation" | i18n }}

      -
      -
      - - {{ "providerName" | i18n }} - - + @if (!(requireProviderPaymentMethodDuringSetup$ | async)) { +

      {{ "generalInformation" | i18n }}

      +
      +
      + + {{ "providerName" | i18n }} + + +
      +
      + + {{ "billingEmail" | i18n }} + + {{ "providerBillingEmailHint" | i18n }} + +
      -
      - - {{ "billingEmail" | i18n }} - - {{ "providerBillingEmailHint" | i18n }} - + + } @else { +

      {{ "billingInformation" | i18n }}

      +
      +
      + + {{ "providerName" | i18n }} + + +
      +
      + + {{ "billingEmail" | i18n }} + + {{ "providerBillingEmailHint" | i18n }} + +
      -
      - +

      {{ "paymentMethod" | i18n }}

      + + + } diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.ts index ecf649b8f31..0b6483b9f48 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.ts @@ -3,13 +3,14 @@ import { Component, OnDestroy, OnInit, ViewChild } from "@angular/core"; import { FormBuilder, Validators } from "@angular/forms"; import { ActivatedRoute, Router } from "@angular/router"; -import { Subject, switchMap } from "rxjs"; +import { firstValueFrom, Subject, switchMap } from "rxjs"; import { first, takeUntil } from "rxjs/operators"; import { ManageTaxInformationComponent } from "@bitwarden/angular/billing/components"; import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction"; import { ProviderSetupRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-setup.request"; import { ExpandedTaxInfoUpdateRequest } from "@bitwarden/common/billing/models/request/expanded-tax-info-update.request"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; @@ -17,12 +18,14 @@ import { ProviderKey } from "@bitwarden/common/types/key"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { ToastService } from "@bitwarden/components"; import { KeyService } from "@bitwarden/key-management"; +import { PaymentComponent } from "@bitwarden/web-vault/app/billing/shared/payment/payment.component"; @Component({ selector: "provider-setup", templateUrl: "setup.component.html", }) export class SetupComponent implements OnInit, OnDestroy { + @ViewChild(PaymentComponent) paymentComponent: PaymentComponent; @ViewChild(ManageTaxInformationComponent) taxInformationComponent: ManageTaxInformationComponent; loading = true; @@ -36,6 +39,10 @@ export class SetupComponent implements OnInit, OnDestroy { private destroy$ = new Subject(); + requireProviderPaymentMethodDuringSetup$ = this.configService.getFeatureFlag$( + FeatureFlag.PM19956_RequireProviderPaymentMethodDuringSetup, + ); + constructor( private router: Router, private i18nService: I18nService, @@ -134,6 +141,14 @@ export class SetupComponent implements OnInit, OnDestroy { request.taxInfo.city = taxInformation.city; request.taxInfo.state = taxInformation.state; + const requireProviderPaymentMethodDuringSetup = await firstValueFrom( + this.requireProviderPaymentMethodDuringSetup$, + ); + + if (requireProviderPaymentMethodDuringSetup) { + request.paymentSource = await this.paymentComponent.tokenize(); + } + const provider = await this.providerApiService.postProviderSetup(this.providerId, request); this.toastService.showToast({ diff --git a/libs/common/src/admin-console/models/request/provider/provider-setup.request.ts b/libs/common/src/admin-console/models/request/provider/provider-setup.request.ts index d4992e969dc..5c9ea5526a0 100644 --- a/libs/common/src/admin-console/models/request/provider/provider-setup.request.ts +++ b/libs/common/src/admin-console/models/request/provider/provider-setup.request.ts @@ -1,6 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { ExpandedTaxInfoUpdateRequest } from "../../../../billing/models/request/expanded-tax-info-update.request"; +import { TokenizedPaymentSourceRequest } from "../../../../billing/models/request/tokenized-payment-source.request"; export class ProviderSetupRequest { name: string; @@ -9,4 +10,5 @@ export class ProviderSetupRequest { token: string; key: string; taxInfo: ExpandedTaxInfoUpdateRequest; + paymentSource?: TokenizedPaymentSourceRequest; } diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index 0e0956b0460..3644ceefa9a 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -34,6 +34,7 @@ export enum FeatureFlag { PM12276_BreadcrumbEventLogs = "pm-12276-breadcrumbing-for-business-features", PM18794_ProviderPaymentMethod = "pm-18794-provider-payment-method", PM17772_AdminInitiatedSponsorships = "pm-17772-admin-initiated-sponsorships", + PM19956_RequireProviderPaymentMethodDuringSetup = "pm-19956-require-provider-payment-method-during-setup", /* Data Insights and Reporting */ CriticalApps = "pm-14466-risk-insights-critical-application", @@ -121,6 +122,7 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.PM12276_BreadcrumbEventLogs]: FALSE, [FeatureFlag.PM18794_ProviderPaymentMethod]: FALSE, [FeatureFlag.PM17772_AdminInitiatedSponsorships]: FALSE, + [FeatureFlag.PM19956_RequireProviderPaymentMethodDuringSetup]: FALSE, /* Key Management */ [FeatureFlag.PrivateKeyRegeneration]: FALSE, From a62d269a8910be6f8f34930610389b6ccdec0daa Mon Sep 17 00:00:00 2001 From: Jason Ng Date: Thu, 1 May 2025 12:43:55 -0400 Subject: [PATCH 106/499] [PM-18803] nudges new items (#14523) * Added new-items-nudge service and component to show spotlight for new item nudges --- apps/browser/src/_locales/en/messages.json | 32 +++++- apps/desktop/src/locales/en/messages.json | 30 ++++++ apps/web/src/locales/en/messages.json | 30 ++++++ .../src/platform/state/state-definitions.ts | 2 +- .../src/cipher-form/cipher-form.stories.ts | 20 +++- .../components/cipher-form.component.html | 1 + .../components/cipher-form.component.ts | 2 + .../new-item-nudge.component.html | 8 ++ .../new-item-nudge.component.spec.ts | 101 ++++++++++++++++++ .../new-item-nudge.component.ts | 90 ++++++++++++++++ .../has-nudge.service.ts | 2 - .../services/custom-nudges-services/index.ts | 1 + .../new-item-nudge.service.ts | 65 +++++++++++ .../src/services/vault-nudges.service.spec.ts | 2 + .../src/services/vault-nudges.service.ts | 19 +++- 15 files changed, 398 insertions(+), 7 deletions(-) create mode 100644 libs/vault/src/cipher-form/components/new-item-nudge/new-item-nudge.component.html create mode 100644 libs/vault/src/cipher-form/components/new-item-nudge/new-item-nudge.component.spec.ts create mode 100644 libs/vault/src/cipher-form/components/new-item-nudge/new-item-nudge.component.ts create mode 100644 libs/vault/src/services/custom-nudges-services/new-item-nudge.service.ts diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 5c9e829e82f..6e1e2ef57ac 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -5248,5 +5248,35 @@ }, "hasItemsVaultNudgeBody": { "message": "Autofill items for the current page\nFavorite items for easy access\nSearch your vault for something else" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } -} \ No newline at end of file +} diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index 81e3a94ff4d..2350e0df4c7 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -3712,5 +3712,35 @@ }, "move": { "message": "Move" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } } diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index ce45b538bbe..59ba7961d82 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -10629,6 +10629,36 @@ "newBusinessUnit": { "message": "New business unit" }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." + }, "restart": { "message": "Restart" }, diff --git a/libs/common/src/platform/state/state-definitions.ts b/libs/common/src/platform/state/state-definitions.ts index 70e0c3998dd..587212299df 100644 --- a/libs/common/src/platform/state/state-definitions.ts +++ b/libs/common/src/platform/state/state-definitions.ts @@ -206,7 +206,7 @@ export const VAULT_APPEARANCE = new StateDefinition("vaultAppearance", "disk"); export const SECURITY_TASKS_DISK = new StateDefinition("securityTasks", "disk"); export const AT_RISK_PASSWORDS_PAGE_DISK = new StateDefinition("atRiskPasswordsPage", "disk"); export const NOTIFICATION_DISK = new StateDefinition("notifications", "disk"); -export const VAULT_NUDGES_DISK = new StateDefinition("vaultNudges", "disk"); +export const VAULT_NUDGES_DISK = new StateDefinition("vaultNudges", "disk", { web: "disk-local" }); export const VAULT_BROWSER_INTRO_CAROUSEL = new StateDefinition( "vaultBrowserIntroCarousel", "disk", diff --git a/libs/vault/src/cipher-form/cipher-form.stories.ts b/libs/vault/src/cipher-form/cipher-form.stories.ts index 50577472120..9943f07292d 100644 --- a/libs/vault/src/cipher-form/cipher-form.stories.ts +++ b/libs/vault/src/cipher-form/cipher-form.stories.ts @@ -34,7 +34,9 @@ import { AsyncActionsModule, ButtonModule, ItemModule, ToastService } from "@bit import { CipherFormConfig, CipherFormGenerationService, + NudgeStatus, PasswordRepromptService, + VaultNudgesService, } from "@bitwarden/vault"; // FIXME: remove `/apps` import from `/libs` // FIXME: remove `src` and fix import @@ -47,6 +49,7 @@ import { CipherFormService } from "./abstractions/cipher-form.service"; import { TotpCaptureService } from "./abstractions/totp-capture.service"; import { CipherFormModule } from "./cipher-form.module"; import { CipherFormComponent } from "./components/cipher-form.component"; +import { NewItemNudgeComponent } from "./components/new-item-nudge/new-item-nudge.component"; import { CipherFormCacheService } from "./services/default-cipher-form-cache.service"; const defaultConfig: CipherFormConfig = { @@ -132,8 +135,23 @@ export default { component: CipherFormComponent, decorators: [ moduleMetadata({ - imports: [CipherFormModule, AsyncActionsModule, ButtonModule, ItemModule], + imports: [ + CipherFormModule, + AsyncActionsModule, + ButtonModule, + ItemModule, + NewItemNudgeComponent, + ], providers: [ + { + provide: VaultNudgesService, + useValue: { + showNudge$: new BehaviorSubject({ + hasBadgeDismissed: true, + hasSpotlightDismissed: true, + } as NudgeStatus), + }, + }, { provide: CipherFormService, useClass: TestAddEditFormService, diff --git a/libs/vault/src/cipher-form/components/cipher-form.component.html b/libs/vault/src/cipher-form/components/cipher-form.component.html index 6b327486c47..614c7f3dc7a 100644 --- a/libs/vault/src/cipher-form/components/cipher-form.component.html +++ b/libs/vault/src/cipher-form/components/cipher-form.component.html @@ -1,3 +1,4 @@ + diff --git a/libs/vault/src/cipher-form/components/cipher-form.component.ts b/libs/vault/src/cipher-form/components/cipher-form.component.ts index 080af489253..96e1328338b 100644 --- a/libs/vault/src/cipher-form/components/cipher-form.component.ts +++ b/libs/vault/src/cipher-form/components/cipher-form.component.ts @@ -45,6 +45,7 @@ import { CardDetailsSectionComponent } from "./card-details-section/card-details import { IdentitySectionComponent } from "./identity/identity.component"; import { ItemDetailsSectionComponent } from "./item-details/item-details-section.component"; import { LoginDetailsSectionComponent } from "./login-details-section/login-details-section.component"; +import { NewItemNudgeComponent } from "./new-item-nudge/new-item-nudge.component"; import { SshKeySectionComponent } from "./sshkey-section/sshkey-section.component"; @Component({ @@ -76,6 +77,7 @@ import { SshKeySectionComponent } from "./sshkey-section/sshkey-section.componen NgIf, AdditionalOptionsSectionComponent, LoginDetailsSectionComponent, + NewItemNudgeComponent, ], }) export class CipherFormComponent implements AfterViewInit, OnInit, OnChanges, CipherFormContainer { diff --git a/libs/vault/src/cipher-form/components/new-item-nudge/new-item-nudge.component.html b/libs/vault/src/cipher-form/components/new-item-nudge/new-item-nudge.component.html new file mode 100644 index 00000000000..5cd1246fd36 --- /dev/null +++ b/libs/vault/src/cipher-form/components/new-item-nudge/new-item-nudge.component.html @@ -0,0 +1,8 @@ + + + + diff --git a/libs/vault/src/cipher-form/components/new-item-nudge/new-item-nudge.component.spec.ts b/libs/vault/src/cipher-form/components/new-item-nudge/new-item-nudge.component.spec.ts new file mode 100644 index 00000000000..073c588690d --- /dev/null +++ b/libs/vault/src/cipher-form/components/new-item-nudge/new-item-nudge.component.spec.ts @@ -0,0 +1,101 @@ +import { CommonModule } from "@angular/common"; +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { mock, MockProxy } from "jest-mock-extended"; +import { of } from "rxjs"; + +import { AccountService, Account } from "@bitwarden/common/auth/abstractions/account.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { UserId } from "@bitwarden/common/types/guid"; +import { CipherType } from "@bitwarden/sdk-internal"; + +import { VaultNudgesService, VaultNudgeType } from "../../../services/vault-nudges.service"; + +import { NewItemNudgeComponent } from "./new-item-nudge.component"; + +describe("NewItemNudgeComponent", () => { + let component: NewItemNudgeComponent; + let fixture: ComponentFixture; + + let i18nService: MockProxy; + let accountService: MockProxy; + let vaultNudgesService: MockProxy; + + beforeEach(async () => { + i18nService = mock({ t: (key: string) => key }); + accountService = mock(); + vaultNudgesService = mock(); + + await TestBed.configureTestingModule({ + imports: [NewItemNudgeComponent, CommonModule], + providers: [ + { provide: I18nService, useValue: i18nService }, + { provide: AccountService, useValue: accountService }, + { provide: VaultNudgesService, useValue: vaultNudgesService }, + ], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(NewItemNudgeComponent); + component = fixture.componentInstance; + component.configType = null; // Set to null for initial state + fixture.detectChanges(); + }); + + it("should create", () => { + expect(component).toBeTruthy(); + }); + + it("should set nudge title and body for CipherType.Login type", async () => { + component.configType = CipherType.Login; + accountService.activeAccount$ = of({ id: "test-user-id" as UserId } as Account); + jest.spyOn(component, "checkHasSpotlightDismissed").mockResolvedValue(true); + + await component.ngOnInit(); + + expect(component.showNewItemSpotlight).toBe(true); + expect(component.nudgeTitle).toBe("newLoginNudgeTitle"); + expect(component.nudgeBody).toBe("newLoginNudgeBody"); + expect(component.dismissalNudgeType).toBe(VaultNudgeType.newLoginItemStatus); + }); + + it("should set nudge title and body for CipherType.Card type", async () => { + component.configType = CipherType.Card; + accountService.activeAccount$ = of({ id: "test-user-id" as UserId } as Account); + jest.spyOn(component, "checkHasSpotlightDismissed").mockResolvedValue(true); + + await component.ngOnInit(); + + expect(component.showNewItemSpotlight).toBe(true); + expect(component.nudgeTitle).toBe("newCardNudgeTitle"); + expect(component.nudgeBody).toBe("newCardNudgeBody"); + expect(component.dismissalNudgeType).toBe(VaultNudgeType.newCardItemStatus); + }); + + it("should not show anything if spotlight has been dismissed", async () => { + component.configType = CipherType.Identity; + accountService.activeAccount$ = of({ id: "test-user-id" as UserId } as Account); + jest.spyOn(component, "checkHasSpotlightDismissed").mockResolvedValue(false); + + await component.ngOnInit(); + + expect(component.showNewItemSpotlight).toBe(false); + expect(component.dismissalNudgeType).toBe(VaultNudgeType.newIdentityItemStatus); + }); + + it("should set showNewItemSpotlight to false when user dismisses spotlight", async () => { + component.showNewItemSpotlight = true; + component.dismissalNudgeType = VaultNudgeType.newLoginItemStatus; + component.activeUserId = "test-user-id" as UserId; + + const dismissSpy = jest.spyOn(vaultNudgesService, "dismissNudge").mockResolvedValue(); + + await component.dismissNewItemSpotlight(); + + expect(component.showNewItemSpotlight).toBe(false); + expect(dismissSpy).toHaveBeenCalledWith( + VaultNudgeType.newLoginItemStatus, + component.activeUserId, + ); + }); +}); diff --git a/libs/vault/src/cipher-form/components/new-item-nudge/new-item-nudge.component.ts b/libs/vault/src/cipher-form/components/new-item-nudge/new-item-nudge.component.ts new file mode 100644 index 00000000000..b497585d4fb --- /dev/null +++ b/libs/vault/src/cipher-form/components/new-item-nudge/new-item-nudge.component.ts @@ -0,0 +1,90 @@ +import { NgIf } from "@angular/common"; +import { Component, Input, 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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { UserId } from "@bitwarden/common/types/guid"; +import { CipherType } from "@bitwarden/sdk-internal"; + +import { SpotlightComponent } from "../../../components/spotlight/spotlight.component"; +import { VaultNudgesService, VaultNudgeType } from "../../../services/vault-nudges.service"; + +@Component({ + selector: "vault-new-item-nudge", + templateUrl: "./new-item-nudge.component.html", + standalone: true, + imports: [NgIf, SpotlightComponent], +}) +export class NewItemNudgeComponent implements OnInit { + @Input({ required: true }) configType: CipherType | null = null; + activeUserId: UserId | null = null; + showNewItemSpotlight: boolean = false; + nudgeTitle: string = ""; + nudgeBody: string = ""; + dismissalNudgeType: VaultNudgeType | null = null; + + constructor( + private i18nService: I18nService, + private accountService: AccountService, + private vaultNudgesService: VaultNudgesService, + ) {} + + async ngOnInit() { + this.activeUserId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + + switch (this.configType) { + case CipherType.Login: + this.dismissalNudgeType = VaultNudgeType.newLoginItemStatus; + this.nudgeTitle = this.i18nService.t("newLoginNudgeTitle"); + this.nudgeBody = this.i18nService.t("newLoginNudgeBody"); + break; + + case CipherType.Card: + this.dismissalNudgeType = VaultNudgeType.newCardItemStatus; + this.nudgeTitle = this.i18nService.t("newCardNudgeTitle"); + this.nudgeBody = this.i18nService.t("newCardNudgeBody"); + break; + + case CipherType.Identity: + this.dismissalNudgeType = VaultNudgeType.newIdentityItemStatus; + this.nudgeTitle = this.i18nService.t("newIdentityNudgeTitle"); + this.nudgeBody = this.i18nService.t("newIdentityNudgeBody"); + break; + + case CipherType.SecureNote: + this.dismissalNudgeType = VaultNudgeType.newNoteItemStatus; + this.nudgeTitle = this.i18nService.t("newNoteNudgeTitle"); + this.nudgeBody = this.i18nService.t("newNoteNudgeBody"); + break; + + case CipherType.SshKey: + this.dismissalNudgeType = VaultNudgeType.newSshItemStatus; + this.nudgeTitle = this.i18nService.t("newSshNudgeTitle"); + this.nudgeBody = this.i18nService.t("newSshNudgeBody"); + break; + default: + throw new Error("Unsupported cipher type"); + } + this.showNewItemSpotlight = await this.checkHasSpotlightDismissed( + this.dismissalNudgeType as VaultNudgeType, + this.activeUserId, + ); + } + + async dismissNewItemSpotlight() { + if (this.dismissalNudgeType && this.activeUserId) { + await this.vaultNudgesService.dismissNudge( + this.dismissalNudgeType, + this.activeUserId as UserId, + ); + this.showNewItemSpotlight = false; + } + } + + async checkHasSpotlightDismissed(nudgeType: VaultNudgeType, userId: UserId): Promise { + return !(await firstValueFrom(this.vaultNudgesService.showNudge$(nudgeType, userId))) + .hasSpotlightDismissed; + } +} diff --git a/libs/vault/src/services/custom-nudges-services/has-nudge.service.ts b/libs/vault/src/services/custom-nudges-services/has-nudge.service.ts index 0c14cff002f..c9077a7283b 100644 --- a/libs/vault/src/services/custom-nudges-services/has-nudge.service.ts +++ b/libs/vault/src/services/custom-nudges-services/has-nudge.service.ts @@ -18,8 +18,6 @@ export class HasNudgeService extends DefaultSingleNudgeService { private nudgeTypes: VaultNudgeType[] = [ VaultNudgeType.EmptyVaultNudge, - VaultNudgeType.HasVaultItems, - VaultNudgeType.IntroCarouselDismissal, // add additional nudge types here as needed ]; diff --git a/libs/vault/src/services/custom-nudges-services/index.ts b/libs/vault/src/services/custom-nudges-services/index.ts index 9a1f0acd420..131db023175 100644 --- a/libs/vault/src/services/custom-nudges-services/index.ts +++ b/libs/vault/src/services/custom-nudges-services/index.ts @@ -1,3 +1,4 @@ export * from "./has-items-nudge.service"; export * from "./empty-vault-nudge.service"; export * from "./has-nudge.service"; +export * from "./new-item-nudge.service"; diff --git a/libs/vault/src/services/custom-nudges-services/new-item-nudge.service.ts b/libs/vault/src/services/custom-nudges-services/new-item-nudge.service.ts new file mode 100644 index 00000000000..93ef5d81dc4 --- /dev/null +++ b/libs/vault/src/services/custom-nudges-services/new-item-nudge.service.ts @@ -0,0 +1,65 @@ +import { inject, Injectable } from "@angular/core"; +import { combineLatest, Observable, switchMap } from "rxjs"; + +import { UserId } from "@bitwarden/common/types/guid"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { CipherType } from "@bitwarden/common/vault/enums"; + +import { DefaultSingleNudgeService } from "../default-single-nudge.service"; +import { NudgeStatus, VaultNudgeType } from "../vault-nudges.service"; + +/** + * Custom Nudge Service Checking Nudge Status For Vault New Item Types + */ +@Injectable({ + providedIn: "root", +}) +export class NewItemNudgeService extends DefaultSingleNudgeService { + cipherService = inject(CipherService); + + nudgeStatus$(nudgeType: VaultNudgeType, userId: UserId): Observable { + return combineLatest([ + this.getNudgeStatus$(nudgeType, userId), + this.cipherService.cipherViews$(userId), + ]).pipe( + switchMap(async ([nudgeStatus, ciphers]) => { + if (nudgeStatus.hasSpotlightDismissed) { + return nudgeStatus; + } + + let currentType: CipherType; + + switch (nudgeType) { + case VaultNudgeType.newLoginItemStatus: + currentType = CipherType.Login; + break; + case VaultNudgeType.newCardItemStatus: + currentType = CipherType.Card; + break; + case VaultNudgeType.newIdentityItemStatus: + currentType = CipherType.Identity; + break; + case VaultNudgeType.newNoteItemStatus: + currentType = CipherType.SecureNote; + break; + case VaultNudgeType.newSshItemStatus: + currentType = CipherType.SshKey; + break; + } + + const ciphersBoolean = ciphers.some((cipher) => cipher.type === currentType); + + if (ciphersBoolean) { + const dismissedStatus = { + hasSpotlightDismissed: true, + hasBadgeDismissed: true, + }; + await this.setNudgeStatus(nudgeType, dismissedStatus, userId); + return dismissedStatus; + } + + return nudgeStatus; + }), + ); + } +} diff --git a/libs/vault/src/services/vault-nudges.service.spec.ts b/libs/vault/src/services/vault-nudges.service.spec.ts index a01cac94fb1..69ddf1cdaa0 100644 --- a/libs/vault/src/services/vault-nudges.service.spec.ts +++ b/libs/vault/src/services/vault-nudges.service.spec.ts @@ -5,6 +5,7 @@ import { firstValueFrom, of } from "rxjs"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { StateProvider } from "@bitwarden/common/platform/state"; import { UserId } from "@bitwarden/common/types/guid"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { FakeStateProvider, mockAccountServiceWith } from "../../../common/spec"; @@ -46,6 +47,7 @@ describe("Vault Nudges Service", () => { provide: EmptyVaultNudgeService, useValue: mock(), }, + { provide: CipherService, useValue: mock() }, ], }); }); diff --git a/libs/vault/src/services/vault-nudges.service.ts b/libs/vault/src/services/vault-nudges.service.ts index 28198d17068..98f28af9954 100644 --- a/libs/vault/src/services/vault-nudges.service.ts +++ b/libs/vault/src/services/vault-nudges.service.ts @@ -6,7 +6,11 @@ import { ConfigService } from "@bitwarden/common/platform/abstractions/config/co import { UserKeyDefinition, VAULT_NUDGES_DISK } from "@bitwarden/common/platform/state"; import { UserId } from "@bitwarden/common/types/guid"; -import { HasItemsNudgeService, EmptyVaultNudgeService } from "./custom-nudges-services"; +import { + HasItemsNudgeService, + EmptyVaultNudgeService, + NewItemNudgeService, +} from "./custom-nudges-services"; import { DefaultSingleNudgeService, SingleNudgeService } from "./default-single-nudge.service"; export type NudgeStatus = { @@ -23,7 +27,11 @@ export enum VaultNudgeType { */ EmptyVaultNudge = "empty-vault-nudge", HasVaultItems = "has-vault-items", - IntroCarouselDismissal = "intro-carousel-dismissal", + newLoginItemStatus = "new-login-item-status", + newCardItemStatus = "new-card-item-status", + newIdentityItemStatus = "new-identity-item-status", + newNoteItemStatus = "new-note-item-status", + newSshItemStatus = "new-ssh-item-status", } export const VAULT_NUDGE_DISMISSED_DISK_KEY = new UserKeyDefinition< @@ -37,6 +45,8 @@ export const VAULT_NUDGE_DISMISSED_DISK_KEY = new UserKeyDefinition< providedIn: "root", }) export class VaultNudgesService { + private newItemNudgeService = inject(NewItemNudgeService); + /** * Custom nudge services to use for specific nudge types * Each nudge type can have its own service to determine when to show the nudge @@ -45,6 +55,11 @@ export class VaultNudgesService { private customNudgeServices: any = { [VaultNudgeType.HasVaultItems]: inject(HasItemsNudgeService), [VaultNudgeType.EmptyVaultNudge]: inject(EmptyVaultNudgeService), + [VaultNudgeType.newLoginItemStatus]: this.newItemNudgeService, + [VaultNudgeType.newCardItemStatus]: this.newItemNudgeService, + [VaultNudgeType.newIdentityItemStatus]: this.newItemNudgeService, + [VaultNudgeType.newNoteItemStatus]: this.newItemNudgeService, + [VaultNudgeType.newSshItemStatus]: this.newItemNudgeService, }; /** From 1123a5993eac31493bd691fc106ceb2062ebaaec Mon Sep 17 00:00:00 2001 From: Jason Ng Date: Thu, 1 May 2025 12:44:13 -0400 Subject: [PATCH 107/499] [PM-20423] Update Empty Vault Button on Browser to follow Web (#14574) * update button flow for new login item in an empty vault on browser --- .../popup/components/vault-v2/vault-v2.component.html | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.html b/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.html index 7d04f23795e..894f27245b2 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.html +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.html @@ -15,12 +15,11 @@ {{ "yourVaultIsEmpty" | i18n }} -

      {{ "emptyVaultDescription" | i18n }}

      +

      {{ "emptyVaultDescription" | i18n }}

      - +
      + {{ "newLogin" | i18n }} +
      From de6b58c10ab6e24adbc6bbb8ae625107e4d86ff0 Mon Sep 17 00:00:00 2001 From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> Date: Thu, 1 May 2025 12:44:08 -0500 Subject: [PATCH 108/499] [PM-20110] Disabled copy buttons on vault (#14549) * export BitIconButtonComponent from component library * manually update the disabled state of the icon button for copy cipher field directive * add tests for `CopyCipherFieldDirective` --- libs/components/src/icon-button/index.ts | 1 + .../copy-cipher-field.directive.spec.ts | 197 ++++++++++++++++++ .../components/copy-cipher-field.directive.ts | 8 +- 3 files changed, 205 insertions(+), 1 deletion(-) create mode 100644 libs/vault/src/components/copy-cipher-field.directive.spec.ts diff --git a/libs/components/src/icon-button/index.ts b/libs/components/src/icon-button/index.ts index 9da4a3162bf..cc52f263252 100644 --- a/libs/components/src/icon-button/index.ts +++ b/libs/components/src/icon-button/index.ts @@ -1 +1,2 @@ export * from "./icon-button.module"; +export { BitIconButtonComponent } from "./icon-button.component"; diff --git a/libs/vault/src/components/copy-cipher-field.directive.spec.ts b/libs/vault/src/components/copy-cipher-field.directive.spec.ts new file mode 100644 index 00000000000..0847e7147a9 --- /dev/null +++ b/libs/vault/src/components/copy-cipher-field.directive.spec.ts @@ -0,0 +1,197 @@ +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { BitIconButtonComponent, MenuItemDirective } from "@bitwarden/components"; +import { CopyCipherFieldService } from "@bitwarden/vault"; + +import { CopyCipherFieldDirective } from "./copy-cipher-field.directive"; + +describe("CopyCipherFieldDirective", () => { + const copyFieldService = { + copy: jest.fn().mockResolvedValue(null), + totpAllowed: jest.fn().mockResolvedValue(true), + }; + + let copyCipherFieldDirective: CopyCipherFieldDirective; + + beforeEach(() => { + copyFieldService.copy.mockClear(); + copyFieldService.totpAllowed.mockClear(); + + copyCipherFieldDirective = new CopyCipherFieldDirective( + copyFieldService as unknown as CopyCipherFieldService, + ); + copyCipherFieldDirective.cipher = new CipherView(); + }); + + describe("disabled state", () => { + it("should be enabled when the field is available", async () => { + copyCipherFieldDirective.action = "username"; + copyCipherFieldDirective.cipher.login.username = "test-username"; + + await copyCipherFieldDirective.ngOnChanges(); + + expect(copyCipherFieldDirective["disabled"]).toBe(null); + }); + + it("should be disabled when the field is not available", async () => { + // create empty cipher + copyCipherFieldDirective.cipher = new CipherView(); + + copyCipherFieldDirective.action = "username"; + + await copyCipherFieldDirective.ngOnChanges(); + + expect(copyCipherFieldDirective["disabled"]).toBe(true); + }); + + it("updates icon button disabled state", async () => { + const iconButton = { + disabled: { + set: jest.fn(), + }, + }; + + copyCipherFieldDirective = new CopyCipherFieldDirective( + copyFieldService as unknown as CopyCipherFieldService, + undefined, + iconButton as unknown as BitIconButtonComponent, + ); + + copyCipherFieldDirective.action = "password"; + + await copyCipherFieldDirective.ngOnChanges(); + + expect(iconButton.disabled.set).toHaveBeenCalledWith(true); + }); + + it("updates menuItemDirective disabled state", async () => { + const menuItemDirective = { + disabled: false, + }; + + copyCipherFieldDirective = new CopyCipherFieldDirective( + copyFieldService as unknown as CopyCipherFieldService, + menuItemDirective as unknown as MenuItemDirective, + ); + + copyCipherFieldDirective.action = "totp"; + + await copyCipherFieldDirective.ngOnChanges(); + + expect(menuItemDirective.disabled).toBe(true); + }); + }); + + describe("login", () => { + beforeEach(() => { + copyCipherFieldDirective.cipher.login.username = "test-username"; + copyCipherFieldDirective.cipher.login.password = "test-password"; + copyCipherFieldDirective.cipher.login.totp = "test-totp"; + }); + + it.each([ + ["username", "test-username"], + ["password", "test-password"], + ["totp", "test-totp"], + ])("copies %s field from login to clipboard", async (action, value) => { + copyCipherFieldDirective.action = action as CopyCipherFieldDirective["action"]; + + await copyCipherFieldDirective.copy(); + + expect(copyFieldService.copy).toHaveBeenCalledWith( + value, + action, + copyCipherFieldDirective.cipher, + ); + }); + }); + + describe("identity", () => { + beforeEach(() => { + copyCipherFieldDirective.cipher.identity.username = "test-username"; + copyCipherFieldDirective.cipher.identity.email = "test-email"; + copyCipherFieldDirective.cipher.identity.phone = "test-phone"; + copyCipherFieldDirective.cipher.identity.address1 = "test-address-1"; + }); + + it.each([ + ["username", "test-username"], + ["email", "test-email"], + ["phone", "test-phone"], + ["address", "test-address-1"], + ])("copies %s field from identity to clipboard", async (action, value) => { + copyCipherFieldDirective.action = action as CopyCipherFieldDirective["action"]; + + await copyCipherFieldDirective.copy(); + + expect(copyFieldService.copy).toHaveBeenCalledWith( + value, + action, + copyCipherFieldDirective.cipher, + ); + }); + }); + + describe("card", () => { + beforeEach(() => { + copyCipherFieldDirective.cipher.card.number = "test-card-number"; + copyCipherFieldDirective.cipher.card.code = "test-card-code"; + }); + + it.each([ + ["cardNumber", "test-card-number"], + ["securityCode", "test-card-code"], + ])("copies %s field from card to clipboard", async (action, value) => { + copyCipherFieldDirective.action = action as CopyCipherFieldDirective["action"]; + + await copyCipherFieldDirective.copy(); + + expect(copyFieldService.copy).toHaveBeenCalledWith( + value, + action, + copyCipherFieldDirective.cipher, + ); + }); + }); + + describe("secure note", () => { + beforeEach(() => { + copyCipherFieldDirective.cipher.notes = "test-secure-note"; + }); + + it("copies secure note field to clipboard", async () => { + copyCipherFieldDirective.action = "secureNote"; + + await copyCipherFieldDirective.copy(); + + expect(copyFieldService.copy).toHaveBeenCalledWith( + "test-secure-note", + "secureNote", + copyCipherFieldDirective.cipher, + ); + }); + }); + + describe("ssh key", () => { + beforeEach(() => { + copyCipherFieldDirective.cipher.sshKey.privateKey = "test-private-key"; + copyCipherFieldDirective.cipher.sshKey.publicKey = "test-public-key"; + copyCipherFieldDirective.cipher.sshKey.keyFingerprint = "test-key-fingerprint"; + }); + + it.each([ + ["privateKey", "test-private-key"], + ["publicKey", "test-public-key"], + ["keyFingerprint", "test-key-fingerprint"], + ])("copies %s field from ssh key to clipboard", async (action, value) => { + copyCipherFieldDirective.action = action as CopyCipherFieldDirective["action"]; + + await copyCipherFieldDirective.copy(); + + expect(copyFieldService.copy).toHaveBeenCalledWith( + value, + action, + copyCipherFieldDirective.cipher, + ); + }); + }); +}); diff --git a/libs/vault/src/components/copy-cipher-field.directive.ts b/libs/vault/src/components/copy-cipher-field.directive.ts index 1eb96a30449..324b43f12d4 100644 --- a/libs/vault/src/components/copy-cipher-field.directive.ts +++ b/libs/vault/src/components/copy-cipher-field.directive.ts @@ -1,7 +1,7 @@ import { Directive, HostBinding, HostListener, Input, OnChanges, Optional } from "@angular/core"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; -import { MenuItemDirective } from "@bitwarden/components"; +import { MenuItemDirective, BitIconButtonComponent } from "@bitwarden/components"; import { CopyAction, CopyCipherFieldService } from "@bitwarden/vault"; /** @@ -33,6 +33,7 @@ export class CopyCipherFieldDirective implements OnChanges { constructor( private copyCipherFieldService: CopyCipherFieldService, @Optional() private menuItemDirective?: MenuItemDirective, + @Optional() private iconButtonComponent?: BitIconButtonComponent, ) {} @HostBinding("attr.disabled") @@ -65,6 +66,11 @@ export class CopyCipherFieldDirective implements OnChanges { ? true : null; + // When used on an icon button, update the disabled state of the button component + if (this.iconButtonComponent) { + this.iconButtonComponent.disabled.set(this.disabled ?? false); + } + // If the directive is used on a menu item, update the menu item to prevent keyboard navigation if (this.menuItemDirective) { this.menuItemDirective.disabled = this.disabled ?? false; From cba5f826d665f201e9f454344fbec780de0e275c Mon Sep 17 00:00:00 2001 From: Shane Melton Date: Thu, 1 May 2025 11:22:32 -0700 Subject: [PATCH 109/499] [PM-21041] Fix cipher view security tasks fetching (#14569) * [PM-21041] Add taskEnabled$ dependency to tasks$ observable * [PM-21041] Rework cipher view component to only check tasks for organization Login type ciphers - Remove dependency on feature flag check (handled by tasks$ observable now) - Add try/catch in case of request failures to avoid breaking component initialization * [PM-21041] Remove now redundant taskEnabled$ chain * [PM-21041] Fix tests --- .../at-risk-password-callout.component.ts | 20 +------ .../emergency-view-dialog.component.spec.ts | 22 ++++++-- .../services/default-task.service.spec.ts | 56 +++++++++++++++++++ .../tasks/services/default-task.service.ts | 40 +++++++++---- .../cipher-view/cipher-view.component.html | 29 +++++----- .../src/cipher-view/cipher-view.component.ts | 53 ++++++++++-------- 6 files changed, 147 insertions(+), 73 deletions(-) diff --git a/apps/browser/src/vault/popup/components/at-risk-callout/at-risk-password-callout.component.ts b/apps/browser/src/vault/popup/components/at-risk-callout/at-risk-password-callout.component.ts index fa4137d9849..ed78d9433f1 100644 --- a/apps/browser/src/vault/popup/components/at-risk-callout/at-risk-password-callout.component.ts +++ b/apps/browser/src/vault/popup/components/at-risk-callout/at-risk-password-callout.component.ts @@ -1,7 +1,7 @@ import { CommonModule } from "@angular/common"; import { Component, inject } from "@angular/core"; import { RouterModule } from "@angular/router"; -import { map, of, switchMap } from "rxjs"; +import { map, switchMap } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; @@ -20,21 +20,7 @@ export class AtRiskPasswordCalloutComponent { private activeAccount$ = inject(AccountService).activeAccount$.pipe(getUserId); protected pendingTasks$ = this.activeAccount$.pipe( - switchMap((userId) => - this.taskService.tasksEnabled$(userId).pipe( - switchMap((enabled) => { - if (!enabled) { - return of([]); - } - return this.taskService - .pendingTasks$(userId) - .pipe( - map((tasks) => - tasks.filter((t) => t.type === SecurityTaskType.UpdateAtRiskCredential), - ), - ); - }), - ), - ), + switchMap((userId) => this.taskService.pendingTasks$(userId)), + map((tasks) => tasks.filter((t) => t.type === SecurityTaskType.UpdateAtRiskCredential)), ); } diff --git a/apps/web/src/app/auth/settings/emergency-access/view/emergency-view-dialog.component.spec.ts b/apps/web/src/app/auth/settings/emergency-access/view/emergency-view-dialog.component.spec.ts index 3ffb54f5b1c..b341fc4f8e4 100644 --- a/apps/web/src/app/auth/settings/emergency-access/view/emergency-view-dialog.component.spec.ts +++ b/apps/web/src/app/auth/settings/emergency-access/view/emergency-view-dialog.component.spec.ts @@ -8,14 +8,16 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; -import { UserId } from "@bitwarden/common/types/guid"; +import { UserId, EmergencyAccessId } 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 { CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { LoginView } from "@bitwarden/common/vault/models/view/login.view"; import { TaskService } from "@bitwarden/common/vault/tasks"; import { DialogService, DialogRef, DIALOG_DATA } from "@bitwarden/components"; import { ChangeLoginPasswordService } from "@bitwarden/vault"; @@ -28,14 +30,15 @@ describe("EmergencyViewDialogComponent", () => { const open = jest.fn(); const close = jest.fn(); + const emergencyAccessId = "emergency-access-id" as EmergencyAccessId; const mockCipher = { id: "cipher1", name: "Cipher", type: CipherType.Login, - login: { uris: [] }, + login: { uris: [] } as Partial, card: {}, - } as CipherView; + } as Partial as CipherView; const accountService: FakeAccountService = mockAccountServiceWith(Utils.newGuid() as UserId); @@ -56,6 +59,7 @@ describe("EmergencyViewDialogComponent", () => { { provide: DIALOG_DATA, useValue: { cipher: mockCipher } }, { provide: AccountService, useValue: accountService }, { provide: TaskService, useValue: mock() }, + { provide: LogService, useValue: mock() }, ], }) .overrideComponent(EmergencyViewDialogComponent, { @@ -94,18 +98,24 @@ describe("EmergencyViewDialogComponent", () => { }); it("opens dialog", () => { - EmergencyViewDialogComponent.open({ open } as unknown as DialogService, { cipher: mockCipher }); + EmergencyViewDialogComponent.open({ open } as unknown as DialogService, { + cipher: mockCipher, + emergencyAccessId, + }); expect(open).toHaveBeenCalled(); }); it("closes the dialog", () => { - EmergencyViewDialogComponent.open({ open } as unknown as DialogService, { cipher: mockCipher }); + EmergencyViewDialogComponent.open({ open } as unknown as DialogService, { + cipher: mockCipher, + emergencyAccessId, + }); fixture.detectChanges(); const cancelButton = fixture.debugElement.queryAll(By.css("button")).pop(); - cancelButton.nativeElement.click(); + cancelButton!.nativeElement.click(); expect(close).toHaveBeenCalled(); }); diff --git a/libs/common/src/vault/tasks/services/default-task.service.spec.ts b/libs/common/src/vault/tasks/services/default-task.service.spec.ts index 4d468d09766..cb22d1296ba 100644 --- a/libs/common/src/vault/tasks/services/default-task.service.spec.ts +++ b/libs/common/src/vault/tasks/services/default-task.service.spec.ts @@ -108,6 +108,34 @@ describe("Default task service", () => { }); describe("tasks$", () => { + beforeEach(() => { + mockGetFeatureFlag$.mockReturnValue(new BehaviorSubject(true)); + mockGetAllOrgs$.mockReturnValue( + new BehaviorSubject([ + { + useRiskInsights: true, + }, + ] as Organization[]), + ); + }); + + it("should return an empty array if tasks are not enabled", async () => { + mockGetAllOrgs$.mockReturnValue( + new BehaviorSubject([ + { + useRiskInsights: false, + }, + ] as Organization[]), + ); + + const { tasks$ } = service; + + const result = await firstValueFrom(tasks$("user-id" as UserId)); + + expect(result.length).toBe(0); + expect(mockApiSend).not.toHaveBeenCalled(); + }); + it("should fetch tasks from the API when the state is null", async () => { mockApiSend.mockResolvedValue({ data: [ @@ -153,6 +181,34 @@ describe("Default task service", () => { }); describe("pendingTasks$", () => { + beforeEach(() => { + mockGetFeatureFlag$.mockReturnValue(new BehaviorSubject(true)); + mockGetAllOrgs$.mockReturnValue( + new BehaviorSubject([ + { + useRiskInsights: true, + }, + ] as Organization[]), + ); + }); + + it("should return an empty array if tasks are not enabled", async () => { + mockGetAllOrgs$.mockReturnValue( + new BehaviorSubject([ + { + useRiskInsights: false, + }, + ] as Organization[]), + ); + + const { pendingTasks$ } = service; + + const result = await firstValueFrom(pendingTasks$("user-id" as UserId)); + + expect(result.length).toBe(0); + expect(mockApiSend).not.toHaveBeenCalled(); + }); + it("should filter tasks to only pending tasks", async () => { fakeStateProvider.singleUser.mockFor("user-id" as UserId, SECURITY_TASKS, [ { diff --git a/libs/common/src/vault/tasks/services/default-task.service.ts b/libs/common/src/vault/tasks/services/default-task.service.ts index 7386102263c..a50f736f7fd 100644 --- a/libs/common/src/vault/tasks/services/default-task.service.ts +++ b/libs/common/src/vault/tasks/services/default-task.service.ts @@ -1,4 +1,14 @@ -import { combineLatest, filter, map, merge, Observable, of, Subscription, switchMap } from "rxjs"; +import { + combineLatest, + filter, + map, + merge, + Observable, + of, + Subscription, + switchMap, + distinctUntilChanged, +} from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; @@ -45,20 +55,30 @@ export class DefaultTaskService implements TaskService { .organizations$(userId) .pipe(map((orgs) => orgs.some((o) => o.useRiskInsights))), this.configService.getFeatureFlag$(FeatureFlag.SecurityTasks), - ]).pipe(map(([atLeastOneOrgEnabled, flagEnabled]) => atLeastOneOrgEnabled && flagEnabled)); + ]).pipe( + map(([atLeastOneOrgEnabled, flagEnabled]) => atLeastOneOrgEnabled && flagEnabled), + distinctUntilChanged(), + ); }); tasks$ = perUserCache$((userId) => { - return this.taskState(userId).state$.pipe( - switchMap(async (tasks) => { - if (tasks == null) { - await this.fetchTasksFromApi(userId); - return null; + return this.tasksEnabled$(userId).pipe( + switchMap((enabled) => { + if (!enabled) { + return of([]); } - return tasks; + return this.taskState(userId).state$.pipe( + switchMap(async (tasks) => { + if (tasks == null) { + await this.fetchTasksFromApi(userId); + return null; + } + return tasks; + }), + filterOutNullish(), + map((tasks) => tasks.map((t) => new SecurityTask(t))), + ); }), - filterOutNullish(), - map((tasks) => tasks.map((t) => new SecurityTask(t))), ); }); diff --git a/libs/vault/src/cipher-view/cipher-view.component.html b/libs/vault/src/cipher-view/cipher-view.component.html index 99f1b1e47f5..68edd265d09 100644 --- a/libs/vault/src/cipher-view/cipher-view.component.html +++ b/libs/vault/src/cipher-view/cipher-view.component.html @@ -3,19 +3,18 @@ {{ "cardExpiredMessage" | i18n }} - - - - - {{ "changeAtRiskPassword" | i18n }} - - - - + + + + {{ "changeAtRiskPassword" | i18n }} + + + +

      diff --git a/libs/vault/src/cipher-view/cipher-view.component.ts b/libs/vault/src/cipher-view/cipher-view.component.ts index e748fa46656..cf78e78a65f 100644 --- a/libs/vault/src/cipher-view/cipher-view.component.ts +++ b/libs/vault/src/cipher-view/cipher-view.component.ts @@ -12,12 +12,12 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { isCardExpired } from "@bitwarden/common/autofill/utils"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { CipherId, CollectionId, EmergencyAccessId, UserId } 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 { CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; import { SecurityTaskType, TaskService } from "@bitwarden/common/vault/tasks"; @@ -80,7 +80,6 @@ export class CipherViewComponent implements OnChanges, OnDestroy { private destroyed$: Subject = new Subject(); cardIsExpired: boolean = false; hadPendingChangePasswordTask: boolean = false; - isSecurityTasksEnabled$ = this.configService.getFeatureFlag$(FeatureFlag.SecurityTasks); constructor( private organizationService: OrganizationService, @@ -90,8 +89,8 @@ export class CipherViewComponent implements OnChanges, OnDestroy { private defaultTaskService: TaskService, private platformUtilsService: PlatformUtilsService, private changeLoginPasswordService: ChangeLoginPasswordService, - private configService: ConfigService, private cipherService: CipherService, + private logService: LogService, ) {} async ngOnChanges() { @@ -158,20 +157,15 @@ export class CipherViewComponent implements OnChanges, OnDestroy { const userId = await firstValueFrom(this.activeUserId$); - // Show Tasks for Manage and Edit permissions - // Using cipherService to see if user has access to cipher in a non-AC context to address with Edit Except Password permissions - const allCiphers = await firstValueFrom(this.cipherService.ciphers$(userId)); - const cipherServiceCipher = allCiphers[this.cipher?.id as CipherId]; - - if (cipherServiceCipher?.edit && cipherServiceCipher?.viewPassword) { - await this.checkPendingChangePasswordTasks(userId); - } - - if (this.cipher.organizationId && userId) { + if (this.cipher.organizationId) { this.organization$ = this.organizationService .organizations$(userId) .pipe(getOrganizationById(this.cipher.organizationId)) .pipe(takeUntil(this.destroyed$)); + + if (this.cipher.type === CipherType.Login) { + await this.checkPendingChangePasswordTasks(userId); + } } if (this.cipher.folderId) { @@ -182,17 +176,28 @@ export class CipherViewComponent implements OnChanges, OnDestroy { } async checkPendingChangePasswordTasks(userId: UserId): Promise { - if (!(await firstValueFrom(this.isSecurityTasksEnabled$))) { - return; + try { + // Show Tasks for Manage and Edit permissions + // Using cipherService to see if user has access to cipher in a non-AC context to address with Edit Except Password permissions + const allCiphers = await firstValueFrom(this.cipherService.ciphers$(userId)); + const cipherServiceCipher = allCiphers[this.cipher?.id as CipherId]; + + if (!cipherServiceCipher?.edit || !cipherServiceCipher?.viewPassword) { + this.hadPendingChangePasswordTask = false; + return; + } + + const tasks = await firstValueFrom(this.defaultTaskService.pendingTasks$(userId)); + + this.hadPendingChangePasswordTask = tasks?.some((task) => { + return ( + task.cipherId === this.cipher?.id && task.type === SecurityTaskType.UpdateAtRiskCredential + ); + }); + } catch (error) { + this.hadPendingChangePasswordTask = false; + this.logService.error("Failed to retrieve change password tasks for cipher", error); } - - const tasks = await firstValueFrom(this.defaultTaskService.pendingTasks$(userId)); - - this.hadPendingChangePasswordTask = tasks?.some((task) => { - return ( - task.cipherId === this.cipher?.id && task.type === SecurityTaskType.UpdateAtRiskCredential - ); - }); } launchChangePassword = async () => { From 8ead45f5344d13ae5667d9b97d529675b9037f07 Mon Sep 17 00:00:00 2001 From: Miles Blackwood Date: Thu, 1 May 2025 14:37:24 -0400 Subject: [PATCH 110/499] PM-17391 (#14323) * Establish patterns needing addressing. * Prevent non-password field misidentification (TOTP as username) * Allow 'tel' type for TOTP fill identification * Resolve todo and document. * Remove duplicated line. * Use account creation field type = totp to determine if focused field receives the inline menu for totp. Handles cases where totp can appear alongside login forms and also ensures general cipher and totp cipher inline menus render distinctly when focus changes between the field types necessitating. * Prevent type edge cases. --- .../autofill/background/overlay.background.ts | 26 ++-- .../autofill/enums/autofill-overlay.enum.ts | 1 + .../autofill-overlay-content.service.ts | 5 + .../src/autofill/services/autofill.service.ts | 119 +++++++++--------- 4 files changed, 84 insertions(+), 67 deletions(-) diff --git a/apps/browser/src/autofill/background/overlay.background.ts b/apps/browser/src/autofill/background/overlay.background.ts index 4e2e773a0c7..ab5dd4abb8f 100644 --- a/apps/browser/src/autofill/background/overlay.background.ts +++ b/apps/browser/src/autofill/background/overlay.background.ts @@ -661,20 +661,23 @@ export class OverlayBackground implements OverlayBackgroundInterface { return this.inlineMenuFido2Credentials.has(credentialId); } + /** + * When focused field data contains account creation field type of totp + * and there are totp fields in the current frame for page details return true + * + * @returns boolean + */ private isTotpFieldForCurrentField(): boolean { if (!this.focusedFieldData) { return false; } - const { tabId, frameId } = this.focusedFieldData; - const pageDetailsMap = this.pageDetailsForTab[tabId]; - if (!pageDetailsMap || !pageDetailsMap.has(frameId)) { + const totpFields = this.getTotpFields(); + if (!totpFields) { return false; } - const pageDetail = pageDetailsMap.get(frameId); return ( - pageDetail?.details?.fields?.every((field) => - this.inlineMenuFieldQualificationService.isTotpField(field), - ) || false + totpFields.length > 0 && + this.focusedFieldData?.accountCreationFieldType === InlineMenuAccountCreationFieldType.Totp ); } @@ -1399,7 +1402,7 @@ export class OverlayBackground implements OverlayBackgroundInterface { const pageDetailsMap = this.pageDetailsForTab[currentTabId]; const pageDetails = pageDetailsMap?.get(currentFrameId); - const fields = pageDetails.details.fields; + const fields = pageDetails?.details?.fields || []; const totpFields = fields.filter((f) => this.inlineMenuFieldQualificationService.isTotpField(f), ); @@ -1679,7 +1682,12 @@ export class OverlayBackground implements OverlayBackgroundInterface { !this.focusedFieldMatchesFillType( focusedFieldData?.inlineMenuFillType, previousFocusedFieldData, - ) + ) || + // a TOTP field was just focused to - or unfocused from — a non-TOTP field + // may want to generalize this logic if cipher inline menu types exceed [general cipher, TOTP] + [focusedFieldData, previousFocusedFieldData].filter( + (fd) => fd?.accountCreationFieldType === InlineMenuAccountCreationFieldType.Totp, + ).length === 1 ) { const updateAllCipherTypes = !this.focusedFieldMatchesFillType( CipherType.Login, diff --git a/apps/browser/src/autofill/enums/autofill-overlay.enum.ts b/apps/browser/src/autofill/enums/autofill-overlay.enum.ts index 66ad0da546d..9cc457f3c1a 100644 --- a/apps/browser/src/autofill/enums/autofill-overlay.enum.ts +++ b/apps/browser/src/autofill/enums/autofill-overlay.enum.ts @@ -32,6 +32,7 @@ export const InlineMenuAccountCreationFieldType = { Text: "text", Email: "email", Password: "password", + Totp: "totp", } as const; export type InlineMenuAccountCreationFieldTypes = diff --git a/apps/browser/src/autofill/services/autofill-overlay-content.service.ts b/apps/browser/src/autofill/services/autofill-overlay-content.service.ts index 55260cc1149..dc8f45d104b 100644 --- a/apps/browser/src/autofill/services/autofill-overlay-content.service.ts +++ b/apps/browser/src/autofill/services/autofill-overlay-content.service.ts @@ -1128,6 +1128,11 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ * @param autofillFieldData - Autofill field data captured from the form field element. */ private qualifyAccountCreationFieldType(autofillFieldData: AutofillField) { + if (this.inlineMenuFieldQualificationService.isTotpField(autofillFieldData)) { + autofillFieldData.accountCreationFieldType = InlineMenuAccountCreationFieldType.Totp; + return; + } + if (!this.inlineMenuFieldQualificationService.isUsernameField(autofillFieldData)) { autofillFieldData.accountCreationFieldType = InlineMenuAccountCreationFieldType.Password; return; diff --git a/apps/browser/src/autofill/services/autofill.service.ts b/apps/browser/src/autofill/services/autofill.service.ts index 0423078fd1c..da46ceb0864 100644 --- a/apps/browser/src/autofill/services/autofill.service.ts +++ b/apps/browser/src/autofill/services/autofill.service.ts @@ -931,28 +931,37 @@ export default class AutofillService implements AutofillServiceInterface { } if (!passwordFields.length) { - // No password fields on this page. Let's try to just fuzzy fill the username. - pageDetails.fields.forEach((f) => { - if ( - !options.skipUsernameOnlyFill && - f.viewable && - (f.type === "text" || f.type === "email" || f.type === "tel") && - AutofillService.fieldIsFuzzyMatch(f, AutoFillConstants.UsernameFieldNames) - ) { - usernames.push(f); + // If there are no passwords, username or TOTP fields may be present. + // username and TOTP fields are mutually exclusive + pageDetails.fields.forEach((field) => { + if (!field.viewable) { + return; } - if ( + const isFillableTotpField = options.allowTotpAutofill && - f.viewable && - (f.type === "text" || f.type === "number") && - (AutofillService.fieldIsFuzzyMatch(f, [ + ["number", "tel", "text"].some((t) => t === field.type) && + (AutofillService.fieldIsFuzzyMatch(field, [ ...AutoFillConstants.TotpFieldNames, ...AutoFillConstants.AmbiguousTotpFieldNames, ]) || - f.autoCompleteType === "one-time-code") - ) { - totps.push(f); + field.autoCompleteType === "one-time-code"); + + const isFillableUsernameField = + !options.skipUsernameOnlyFill && + ["email", "tel", "text"].some((t) => t === field.type) && + AutofillService.fieldIsFuzzyMatch(field, AutoFillConstants.UsernameFieldNames); + + // Prefer more uniquely keyworded fields first. + switch (true) { + case isFillableTotpField: + totps.push(field); + return; + case isFillableUsernameField: + usernames.push(field); + return; + default: + return; } }); } @@ -2903,52 +2912,46 @@ export default class AutofillService implements AutofillServiceInterface { /** * Accepts a field and returns true if the field contains a * value that matches any of the names in the provided list. + * + * Returns boolean and attr of value that was matched as a tuple if showMatch is set to true. + * * @param {AutofillField} field * @param {string[]} names - * @returns {boolean} + * @param {boolean} showMatch + * @returns {boolean | [boolean, { attr: string; value: string }?]} */ - static fieldIsFuzzyMatch(field: AutofillField, names: string[]): boolean { - if (AutofillService.hasValue(field.htmlID) && this.fuzzyMatch(names, field.htmlID)) { - return true; - } - if (AutofillService.hasValue(field.htmlName) && this.fuzzyMatch(names, field.htmlName)) { - return true; - } - if ( - AutofillService.hasValue(field["label-tag"]) && - this.fuzzyMatch(names, field["label-tag"]) - ) { - return true; - } - if (AutofillService.hasValue(field.placeholder) && this.fuzzyMatch(names, field.placeholder)) { - return true; - } - if ( - AutofillService.hasValue(field["label-left"]) && - this.fuzzyMatch(names, field["label-left"]) - ) { - return true; - } - if ( - AutofillService.hasValue(field["label-top"]) && - this.fuzzyMatch(names, field["label-top"]) - ) { - return true; - } - if ( - AutofillService.hasValue(field["label-aria"]) && - this.fuzzyMatch(names, field["label-aria"]) - ) { - return true; - } - if ( - AutofillService.hasValue(field.dataSetValues) && - this.fuzzyMatch(names, field.dataSetValues) - ) { - return true; - } + static fieldIsFuzzyMatch( + field: AutofillField, + names: string[], + showMatch: true, + ): [boolean, { attr: string; value: string }?]; + static fieldIsFuzzyMatch(field: AutofillField, names: string[]): boolean; + static fieldIsFuzzyMatch( + field: AutofillField, + names: string[], + showMatch: boolean = false, + ): boolean | [boolean, { attr: string; value: string }?] { + const attrs = [ + "htmlID", + "htmlName", + "label-tag", + "placeholder", + "label-left", + "label-top", + "label-aria", + "dataSetValues", + ]; - return false; + for (const attr of attrs) { + const value = field[attr]; + if (!AutofillService.hasValue(value)) { + continue; + } + if (this.fuzzyMatch(names, value)) { + return showMatch ? [true, { attr, value }] : true; + } + } + return showMatch ? [false] : false; } /** From 4292744ca85248d18719a4bba2f1b1e95ae1daa5 Mon Sep 17 00:00:00 2001 From: Brandon Treston Date: Thu, 1 May 2025 15:20:22 -0400 Subject: [PATCH 111/499] remove deprecated component (#14582) --- .../manage/user-add-edit.component.html | 124 ----------------- .../manage/user-add-edit.component.ts | 125 ------------------ .../providers/providers.module.ts | 2 - 3 files changed, 251 deletions(-) delete mode 100644 bitwarden_license/bit-web/src/app/admin-console/providers/manage/user-add-edit.component.html delete mode 100644 bitwarden_license/bit-web/src/app/admin-console/providers/manage/user-add-edit.component.ts diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/user-add-edit.component.html b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/user-add-edit.component.html deleted file mode 100644 index 78d80d005c9..00000000000 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/user-add-edit.component.html +++ /dev/null @@ -1,124 +0,0 @@ -

      diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/user-add-edit.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/user-add-edit.component.ts deleted file mode 100644 index 82671a7e418..00000000000 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/user-add-edit.component.ts +++ /dev/null @@ -1,125 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core"; - -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { ProviderUserType } from "@bitwarden/common/admin-console/enums"; -import { PermissionsApi } from "@bitwarden/common/admin-console/models/api/permissions.api"; -import { ProviderUserInviteRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-user-invite.request"; -import { ProviderUserUpdateRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-user-update.request"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { DialogService, ToastService } from "@bitwarden/components"; - -/** - * @deprecated Please use the {@link MembersDialogComponent} instead. - */ -@Component({ - selector: "provider-user-add-edit", - templateUrl: "user-add-edit.component.html", -}) -export class UserAddEditComponent implements OnInit { - @Input() name: string; - @Input() providerUserId: string; - @Input() providerId: string; - @Output() savedUser = new EventEmitter(); - @Output() deletedUser = new EventEmitter(); - - loading = true; - editMode = false; - title: string; - emails: string; - type: ProviderUserType = ProviderUserType.ServiceUser; - permissions = new PermissionsApi(); - showCustom = false; - access: "all" | "selected" = "selected"; - formPromise: Promise; - deletePromise: Promise; - userType = ProviderUserType; - - constructor( - private apiService: ApiService, - private i18nService: I18nService, - private platformUtilsService: PlatformUtilsService, - private logService: LogService, - private dialogService: DialogService, - private toastService: ToastService, - ) {} - - async ngOnInit() { - this.editMode = this.loading = this.providerUserId != null; - - if (this.editMode) { - this.editMode = true; - this.title = this.i18nService.t("editMember"); - try { - const user = await this.apiService.getProviderUser(this.providerId, this.providerUserId); - this.type = user.type; - } catch (e) { - this.logService.error(e); - } - } else { - this.title = this.i18nService.t("inviteMember"); - } - - this.loading = false; - } - - async submit() { - try { - if (this.editMode) { - const request = new ProviderUserUpdateRequest(); - request.type = this.type; - this.formPromise = this.apiService.putProviderUser( - this.providerId, - this.providerUserId, - request, - ); - } else { - const request = new ProviderUserInviteRequest(); - request.emails = this.emails.trim().split(/\s*,\s*/); - request.type = this.type; - this.formPromise = this.apiService.postProviderUserInvite(this.providerId, request); - } - await this.formPromise; - this.toastService.showToast({ - variant: "success", - title: null, - message: this.i18nService.t(this.editMode ? "editedUserId" : "invitedUsers", this.name), - }); - this.savedUser.emit(); - } catch (e) { - this.logService.error(e); - } - } - - async delete() { - if (!this.editMode) { - return; - } - - const confirmed = await this.dialogService.openSimpleDialog({ - title: this.name, - content: { key: "removeUserConfirmation" }, - type: "warning", - }); - - if (!confirmed) { - return false; - } - - try { - this.deletePromise = this.apiService.deleteProviderUser(this.providerId, this.providerUserId); - await this.deletePromise; - this.toastService.showToast({ - variant: "success", - title: null, - message: this.i18nService.t("removedUserId", this.name), - }); - this.deletedUser.emit(); - } catch (e) { - this.logService.error(e); - } - } -} diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/providers.module.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/providers.module.ts index 1c15812edc8..597acb0d4f0 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/providers.module.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/providers.module.ts @@ -30,7 +30,6 @@ import { BulkConfirmDialogComponent } from "./manage/dialogs/bulk-confirm-dialog import { BulkRemoveDialogComponent } from "./manage/dialogs/bulk-remove-dialog.component"; import { EventsComponent } from "./manage/events.component"; import { MembersComponent } from "./manage/members.component"; -import { UserAddEditComponent } from "./manage/user-add-edit.component"; import { ProvidersLayoutComponent } from "./providers-layout.component"; import { ProvidersRoutingModule } from "./providers-routing.module"; import { ProvidersComponent } from "./providers.component"; @@ -67,7 +66,6 @@ import { VerifyRecoverDeleteProviderComponent } from "./verify-recover-delete-pr MembersComponent, SetupComponent, SetupProviderComponent, - UserAddEditComponent, AddEditMemberDialogComponent, AddExistingOrganizationDialogComponent, CreateClientDialogComponent, From 23e7f120fd67ea8bf8ff554cd5e23b24a3ef4717 Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Thu, 1 May 2025 13:03:11 -0700 Subject: [PATCH 112/499] [PM-19579] - add sshKey option to desktop (#14402) * add sshKey option to desktop * fix add new sshKey * revert removal of catch * revert removal of catch --- apps/desktop/src/main/menu/menu.file.ts | 6 ++++ .../app/vault/vault-items-v2.component.html | 4 +++ .../vault/app/vault/vault-v2.component.html | 1 - .../src/vault/app/vault/vault-v2.component.ts | 31 ++++++------------- .../src/vault/app/vault/vault.component.ts | 11 +++++++ 5 files changed, 31 insertions(+), 22 deletions(-) diff --git a/apps/desktop/src/main/menu/menu.file.ts b/apps/desktop/src/main/menu/menu.file.ts index 712e579515e..25db5b695e7 100644 --- a/apps/desktop/src/main/menu/menu.file.ts +++ b/apps/desktop/src/main/menu/menu.file.ts @@ -103,6 +103,12 @@ export class FileMenu extends FirstMenu implements IMenubarMenu { click: () => this.sendMessage("newSecureNote"), accelerator: "CmdOrCtrl+Shift+S", }, + { + id: "typeSshKey", + label: this.localize("typeSshKey"), + click: () => this.sendMessage("newSshKey"), + accelerator: "CmdOrCtrl+Shift+K", + }, ]; } diff --git a/apps/desktop/src/vault/app/vault/vault-items-v2.component.html b/apps/desktop/src/vault/app/vault/vault-items-v2.component.html index ff35e00fb0f..63e648e3cf3 100644 --- a/apps/desktop/src/vault/app/vault/vault-items-v2.component.html +++ b/apps/desktop/src/vault/app/vault/vault-items-v2.component.html @@ -88,5 +88,9 @@ {{ "typeSecureNote" | i18n }} + diff --git a/apps/desktop/src/vault/app/vault/vault-v2.component.html b/apps/desktop/src/vault/app/vault/vault-v2.component.html index 12f52502984..00e225f41d1 100644 --- a/apps/desktop/src/vault/app/vault/vault-v2.component.html +++ b/apps/desktop/src/vault/app/vault/vault-v2.component.html @@ -6,7 +6,6 @@ (onCipherClicked)="viewCipher($event)" (onCipherRightClicked)="viewCipherMenu($event)" (onAddCipher)="addCipher($event)" - (onAddCipherOptions)="addCipherOptions()" >
      diff --git a/apps/desktop/src/vault/app/vault/vault-v2.component.ts b/apps/desktop/src/vault/app/vault/vault-v2.component.ts index 7e799899418..05c6c5e261e 100644 --- a/apps/desktop/src/vault/app/vault/vault-v2.component.ts +++ b/apps/desktop/src/vault/app/vault/vault-v2.component.ts @@ -208,6 +208,9 @@ export class VaultV2Component implements OnInit, OnDestroy { case "newSecureNote": await this.addCipher(CipherType.SecureNote).catch(() => {}); break; + case "newSshKey": + await this.addCipher(CipherType.SshKey).catch(() => {}); + break; case "focusSearch": (document.querySelector("#search") as HTMLInputElement)?.select(); detectChanges = false; @@ -531,28 +534,14 @@ export class VaultV2Component implements OnInit, OnDestroy { this.action = "add"; this.prefillCipherFromFilter(); await this.go().catch(() => {}); - } - addCipherOptions() { - const menu: RendererMenuItem[] = [ - { - label: this.i18nService.t("typeLogin"), - click: () => this.addCipherWithChangeDetection(CipherType.Login), - }, - { - label: this.i18nService.t("typeCard"), - click: () => this.addCipherWithChangeDetection(CipherType.Card), - }, - { - label: this.i18nService.t("typeIdentity"), - click: () => this.addCipherWithChangeDetection(CipherType.Identity), - }, - { - label: this.i18nService.t("typeSecureNote"), - click: () => this.addCipherWithChangeDetection(CipherType.SecureNote), - }, - ]; - invokeMenu(menu); + if (type === CipherType.SshKey) { + this.toastService.showToast({ + variant: "success", + title: "", + message: this.i18nService.t("sshKeyGenerated"), + }); + } } async savedCipher(cipher: CipherView) { diff --git a/apps/desktop/src/vault/app/vault/vault.component.ts b/apps/desktop/src/vault/app/vault/vault.component.ts index a21a285a428..6c0d5ef81d0 100644 --- a/apps/desktop/src/vault/app/vault/vault.component.ts +++ b/apps/desktop/src/vault/app/vault/vault.component.ts @@ -151,6 +151,9 @@ export class VaultComponent implements OnInit, OnDestroy { case "newSecureNote": await this.addCipher(CipherType.SecureNote); break; + case "newSshKey": + await this.addCipher(CipherType.SshKey); + break; case "focusSearch": (document.querySelector("#search") as HTMLInputElement).select(); detectChanges = false; @@ -470,6 +473,14 @@ export class VaultComponent implements OnInit, OnDestroy { this.cipherId = null; this.prefillNewCipherFromFilter(); this.go(); + + if (type === CipherType.SshKey) { + this.toastService.showToast({ + variant: "success", + title: "", + message: this.i18nService.t("sshKeyGenerated"), + }); + } } addCipherOptions() { From c8d6e4899636e1084e68a3658ebb6db3f9ab28f8 Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Thu, 1 May 2025 17:13:18 -0400 Subject: [PATCH 113/499] [PM-18955] Use `OrganizationWarningsService` on AC Collections Page behind FF (#14437) * Add getWarnings to OrganizationBillingApiService * Add OrganizationWarningsService * Add feature flag * Add standalone warning components that consume new service * Add new components to AC collections vault when FF is enabled * Add OrganizationWarningsService spec * Run prettier on spec file * Thomas' feedback --- .../collections/vault.component.html | 15 +- .../collections/vault.component.ts | 47 ++- .../organization-warnings.service.spec.ts | 356 ++++++++++++++++++ .../services/organization-warnings.service.ts | 201 ++++++++++ .../warnings/free-trial-warning.component.ts | 56 +++ .../reseller-renewal-warning.component.ts | 45 +++ ...ization-billing-api.service.abstraction.ts | 4 + .../organization-warnings.response.ts | 97 +++++ .../organization-billing-api.service.ts | 14 + libs/common/src/enums/feature-flag.enum.ts | 2 + 10 files changed, 828 insertions(+), 9 deletions(-) create mode 100644 apps/web/src/app/billing/services/organization-warnings.service.spec.ts create mode 100644 apps/web/src/app/billing/services/organization-warnings.service.ts create mode 100644 apps/web/src/app/billing/warnings/free-trial-warning.component.ts create mode 100644 apps/web/src/app/billing/warnings/reseller-renewal-warning.component.ts create mode 100644 libs/common/src/billing/models/response/organization-warnings.response.ts diff --git a/apps/web/src/app/admin-console/organizations/collections/vault.component.html b/apps/web/src/app/admin-console/organizations/collections/vault.component.html index 604d326bf37..22da9a566f4 100644 --- a/apps/web/src/app/admin-console/organizations/collections/vault.component.html +++ b/apps/web/src/app/admin-console/organizations/collections/vault.component.html @@ -1,4 +1,15 @@ - + + + + + - + (false); protected currentSearchText$: Observable; - protected freeTrial$: Observable; - protected resellerWarning$: Observable; + protected useOrganizationWarningsService$: Observable; + protected freeTrialWhenWarningsServiceDisabled$: Observable; + protected resellerWarningWhenWarningsServiceDisabled$: Observable; protected prevCipherId: string | null = null; protected userId: UserId; /** @@ -255,6 +262,7 @@ export class VaultComponent implements OnInit, OnDestroy { private resellerWarningService: ResellerWarningService, private accountService: AccountService, private billingNotificationService: BillingNotificationService, + private organizationWarningsService: OrganizationWarningsService, ) {} async ngOnInit() { @@ -628,9 +636,23 @@ export class VaultComponent implements OnInit, OnDestroy { ) .subscribe(); - this.unpaidSubscriptionDialog$.pipe(takeUntil(this.destroy$)).subscribe(); + // Billing Warnings + this.useOrganizationWarningsService$ = this.configService.getFeatureFlag$( + FeatureFlag.UseOrganizationWarningsService, + ); - this.freeTrial$ = combineLatest([ + this.useOrganizationWarningsService$ + .pipe( + switchMap((enabled) => + enabled + ? this.organizationWarningsService.showInactiveSubscriptionDialog$(this.organization) + : this.unpaidSubscriptionDialog$, + ), + takeUntil(this.destroy$), + ) + .subscribe(); + + const freeTrial$ = combineLatest([ organization$, this.hasSubscription$.pipe(filter((hasSubscription) => hasSubscription !== null)), ]).pipe( @@ -655,7 +677,12 @@ export class VaultComponent implements OnInit, OnDestroy { filter((result) => result !== null), ); - this.resellerWarning$ = organization$.pipe( + this.freeTrialWhenWarningsServiceDisabled$ = this.useOrganizationWarningsService$.pipe( + filter((enabled) => !enabled), + switchMap(() => freeTrial$), + ); + + const resellerWarning$ = organization$.pipe( filter((org) => org.isOwner), switchMap((org) => from(this.billingApiService.getOrganizationBillingMetadata(org.id)).pipe( @@ -665,6 +692,12 @@ export class VaultComponent implements OnInit, OnDestroy { map(({ org, metadata }) => this.resellerWarningService.getWarning(org, metadata)), ); + this.resellerWarningWhenWarningsServiceDisabled$ = this.useOrganizationWarningsService$.pipe( + filter((enabled) => !enabled), + switchMap(() => resellerWarning$), + ); + // End Billing Warnings + firstSetup$ .pipe( switchMap(() => this.refresh$), diff --git a/apps/web/src/app/billing/services/organization-warnings.service.spec.ts b/apps/web/src/app/billing/services/organization-warnings.service.spec.ts new file mode 100644 index 00000000000..88c571e2d67 --- /dev/null +++ b/apps/web/src/app/billing/services/organization-warnings.service.spec.ts @@ -0,0 +1,356 @@ +import { Router } from "@angular/router"; +import { mock, MockProxy } from "jest-mock-extended"; +import { firstValueFrom, lastValueFrom } from "rxjs"; + +import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { OrganizationBillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/organizations/organization-billing-api.service.abstraction"; +import { OrganizationWarningsResponse } from "@bitwarden/common/billing/models/response/organization-warnings.response"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { DialogService, SimpleDialogOptions } from "@bitwarden/components"; + +import { OrganizationWarningsService } from "./organization-warnings.service"; + +describe("OrganizationWarningsService", () => { + let dialogService: MockProxy; + let i18nService: MockProxy; + let organizationApiService: MockProxy; + let organizationBillingApiService: MockProxy; + let router: MockProxy; + + let organizationWarningsService: OrganizationWarningsService; + + const respond = (responseBody: any) => + Promise.resolve(new OrganizationWarningsResponse(responseBody)); + + const empty = () => Promise.resolve(new OrganizationWarningsResponse({})); + + beforeEach(() => { + dialogService = mock(); + i18nService = mock(); + organizationApiService = mock(); + organizationBillingApiService = mock(); + router = mock(); + + organizationWarningsService = new OrganizationWarningsService( + dialogService, + i18nService, + organizationApiService, + organizationBillingApiService, + router, + ); + }); + + describe("cache$", () => { + it("should only request warnings once for a specific organization and replay the cached result for multiple subscriptions", async () => { + const response1 = respond({ + freeTrial: { + remainingTrialDays: 1, + }, + }); + + const organization1 = { + id: "1", + name: "Test", + } as Organization; + + const response2 = respond({ + freeTrial: { + remainingTrialDays: 2, + }, + }); + + const organization2 = { + id: "2", + name: "Test", + } as Organization; + + organizationBillingApiService.getWarnings.mockImplementation((id) => { + if (id === organization1.id) { + return response1; + } + + if (id === organization2.id) { + return response2; + } + + return empty(); + }); + + const oneDayRemainingTranslation = "oneDayRemaining"; + const twoDaysRemainingTranslation = "twoDaysRemaining"; + + i18nService.t.mockImplementation((id, p1) => { + if (id === "freeTrialEndPromptTomorrowNoOrgName") { + return oneDayRemainingTranslation; + } + + if (id === "freeTrialEndPromptCount" && p1 === 2) { + return twoDaysRemainingTranslation; + } + + return ""; + }); + + const organization1Subscription1 = await firstValueFrom( + organizationWarningsService.getFreeTrialWarning$(organization1), + ); + + const organization1Subscription2 = await firstValueFrom( + organizationWarningsService.getFreeTrialWarning$(organization1), + ); + + expect(organization1Subscription1).toEqual({ + organization: organization1, + message: oneDayRemainingTranslation, + }); + + expect(organization1Subscription2).toEqual(organization1Subscription1); + + const organization2Subscription1 = await firstValueFrom( + organizationWarningsService.getFreeTrialWarning$(organization2), + ); + + const organization2Subscription2 = await firstValueFrom( + organizationWarningsService.getFreeTrialWarning$(organization2), + ); + + expect(organization2Subscription1).toEqual({ + organization: organization2, + message: twoDaysRemainingTranslation, + }); + + expect(organization2Subscription2).toEqual(organization2Subscription1); + + expect(organizationBillingApiService.getWarnings).toHaveBeenCalledTimes(2); + }); + }); + + describe("getFreeTrialWarning$", () => { + it("should not emit a free trial warning when none is included in the warnings response", (done) => { + const organization = { + id: "1", + name: "Test", + } as Organization; + + organizationBillingApiService.getWarnings.mockReturnValue(empty()); + + const warning$ = organizationWarningsService.getFreeTrialWarning$(organization); + + warning$.subscribe({ + next: () => { + fail("Observable should not emit a value."); + }, + complete: () => { + done(); + }, + }); + }); + + it("should emit a free trial warning when one is included in the warnings response", async () => { + const response = respond({ + freeTrial: { + remainingTrialDays: 1, + }, + }); + + const organization = { + id: "1", + name: "Test", + } as Organization; + + organizationBillingApiService.getWarnings.mockImplementation((id) => { + if (id === organization.id) { + return response; + } else { + return empty(); + } + }); + + const translation = "translation"; + i18nService.t.mockImplementation((id) => { + if (id === "freeTrialEndPromptTomorrowNoOrgName") { + return translation; + } else { + return ""; + } + }); + + const warning = await firstValueFrom( + organizationWarningsService.getFreeTrialWarning$(organization), + ); + + expect(warning).toEqual({ + organization, + message: translation, + }); + }); + }); + + describe("getResellerRenewalWarning$", () => { + it("should not emit a reseller renewal warning when none is included in the warnings response", (done) => { + const organization = { + id: "1", + name: "Test", + } as Organization; + + organizationBillingApiService.getWarnings.mockReturnValue(empty()); + + const warning$ = organizationWarningsService.getResellerRenewalWarning$(organization); + + warning$.subscribe({ + next: () => { + fail("Observable should not emit a value."); + }, + complete: () => { + done(); + }, + }); + }); + + it("should emit a reseller renewal warning when one is included in the warnings response", async () => { + const response = respond({ + resellerRenewal: { + type: "upcoming", + upcoming: { + renewalDate: "2026-01-01T00:00:00.000Z", + }, + }, + }); + + const organization = { + id: "1", + name: "Test", + providerName: "Provider", + } as Organization; + + organizationBillingApiService.getWarnings.mockImplementation((id) => { + if (id === organization.id) { + return response; + } else { + return empty(); + } + }); + + const formattedDate = new Date("2026-01-01T00:00:00.000Z").toLocaleDateString("en-US", { + month: "short", + day: "2-digit", + year: "numeric", + }); + + const translation = "translation"; + i18nService.t.mockImplementation((id, p1, p2) => { + if ( + id === "resellerRenewalWarningMsg" && + p1 === organization.providerName && + p2 === formattedDate + ) { + return translation; + } else { + return ""; + } + }); + + const warning = await firstValueFrom( + organizationWarningsService.getResellerRenewalWarning$(organization), + ); + + expect(warning).toEqual({ + type: "info", + message: translation, + }); + }); + }); + + describe("showInactiveSubscriptionDialog$", () => { + it("should not emit the opening of a dialog for an inactive subscription warning when the warning is not included in the warnings response", (done) => { + const organization = { + id: "1", + name: "Test", + } as Organization; + + organizationBillingApiService.getWarnings.mockReturnValue(empty()); + + const warning$ = organizationWarningsService.showInactiveSubscriptionDialog$(organization); + + warning$.subscribe({ + next: () => { + fail("Observable should not emit a value."); + }, + complete: () => { + done(); + }, + }); + }); + + it("should emit the opening of a dialog for an inactive subscription warning when the warning is included in the warnings response", async () => { + const response = respond({ + inactiveSubscription: { + resolution: "add_payment_method", + }, + }); + + const organization = { + id: "1", + name: "Test", + providerName: "Provider", + } as Organization; + + organizationBillingApiService.getWarnings.mockImplementation((id) => { + if (id === organization.id) { + return response; + } else { + return empty(); + } + }); + + const titleTranslation = "title"; + const continueTranslation = "continue"; + const closeTranslation = "close"; + + i18nService.t.mockImplementation((id, param) => { + if (id === "suspendedOrganizationTitle" && param === organization.name) { + return titleTranslation; + } + if (id === "continue") { + return continueTranslation; + } + if (id === "close") { + return closeTranslation; + } + return ""; + }); + + const expectedOptions = { + title: titleTranslation, + content: { + key: "suspendedOwnerOrgMessage", + }, + type: "danger", + acceptButtonText: continueTranslation, + cancelButtonText: closeTranslation, + } as SimpleDialogOptions; + + dialogService.openSimpleDialog.mockImplementation((options) => { + if (JSON.stringify(options) == JSON.stringify(expectedOptions)) { + return Promise.resolve(true); + } else { + return Promise.resolve(false); + } + }); + + const observable$ = organizationWarningsService.showInactiveSubscriptionDialog$(organization); + + const routerNavigateSpy = jest.spyOn(router, "navigate").mockResolvedValue(true); + + await lastValueFrom(observable$); + + expect(routerNavigateSpy).toHaveBeenCalledWith( + ["organizations", `${organization.id}`, "billing", "payment-method"], + { + state: { launchPaymentModalAutomatically: true }, + }, + ); + }); + }); +}); diff --git a/apps/web/src/app/billing/services/organization-warnings.service.ts b/apps/web/src/app/billing/services/organization-warnings.service.ts new file mode 100644 index 00000000000..f75220a7744 --- /dev/null +++ b/apps/web/src/app/billing/services/organization-warnings.service.ts @@ -0,0 +1,201 @@ +import { Injectable } from "@angular/core"; +import { Router } from "@angular/router"; +import { + filter, + from, + lastValueFrom, + map, + Observable, + shareReplay, + switchMap, + takeWhile, +} from "rxjs"; +import { take } from "rxjs/operators"; + +import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { OrganizationBillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/organizations/organization-billing-api.service.abstraction"; +import { OrganizationWarningsResponse } from "@bitwarden/common/billing/models/response/organization-warnings.response"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { OrganizationId } from "@bitwarden/common/types/guid"; +import { DialogService } from "@bitwarden/components"; +import { openChangePlanDialog } from "@bitwarden/web-vault/app/billing/organizations/change-plan-dialog.component"; + +const format = (date: Date) => + date.toLocaleDateString("en-US", { + month: "short", + day: "2-digit", + year: "numeric", + }); + +export type FreeTrialWarning = { + organization: Pick; + message: string; +}; + +export type ResellerRenewalWarning = { + type: "info" | "warning"; + message: string; +}; + +@Injectable({ providedIn: "root" }) +export class OrganizationWarningsService { + private cache$ = new Map>(); + + constructor( + private dialogService: DialogService, + private i18nService: I18nService, + private organizationApiService: OrganizationApiServiceAbstraction, + private organizationBillingApiService: OrganizationBillingApiServiceAbstraction, + private router: Router, + ) {} + + getFreeTrialWarning$ = (organization: Organization): Observable => + this.getWarning$(organization, (response) => response.freeTrial).pipe( + map((warning) => { + const { remainingTrialDays } = warning; + + if (remainingTrialDays >= 2) { + return { + organization, + message: this.i18nService.t("freeTrialEndPromptCount", remainingTrialDays), + }; + } + + if (remainingTrialDays == 1) { + return { + organization, + message: this.i18nService.t("freeTrialEndPromptTomorrowNoOrgName"), + }; + } + + return { + organization, + message: this.i18nService.t("freeTrialEndingTodayWithoutOrgName"), + }; + }), + ); + + getResellerRenewalWarning$ = (organization: Organization): Observable => + this.getWarning$(organization, (response) => response.resellerRenewal).pipe( + map((warning): ResellerRenewalWarning | null => { + switch (warning.type) { + case "upcoming": { + return { + type: "info", + message: this.i18nService.t( + "resellerRenewalWarningMsg", + organization.providerName, + format(warning.upcoming!.renewalDate), + ), + }; + } + case "issued": { + return { + type: "info", + message: this.i18nService.t( + "resellerOpenInvoiceWarningMgs", + organization.providerName, + format(warning.issued!.issuedDate), + format(warning.issued!.dueDate), + ), + }; + } + case "past_due": { + return { + type: "warning", + message: this.i18nService.t( + "resellerPastDueWarningMsg", + organization.providerName, + format(warning.pastDue!.suspensionDate), + ), + }; + } + } + }), + filter((result): result is NonNullable => result !== null), + ); + + showInactiveSubscriptionDialog$ = (organization: Organization): Observable => + this.getWarning$(organization, (response) => response.inactiveSubscription).pipe( + switchMap(async (warning) => { + switch (warning.resolution) { + case "contact_provider": { + await this.dialogService.openSimpleDialog({ + title: this.i18nService.t("suspendedOrganizationTitle", organization.name), + content: { + key: "suspendedManagedOrgMessage", + placeholders: [organization.providerName], + }, + type: "danger", + acceptButtonText: this.i18nService.t("close"), + cancelButtonText: null, + }); + break; + } + case "add_payment_method": { + const confirmed = await this.dialogService.openSimpleDialog({ + title: this.i18nService.t("suspendedOrganizationTitle", organization.name), + content: { key: "suspendedOwnerOrgMessage" }, + type: "danger", + acceptButtonText: this.i18nService.t("continue"), + cancelButtonText: this.i18nService.t("close"), + }); + if (confirmed) { + await this.router.navigate( + ["organizations", `${organization.id}`, "billing", "payment-method"], + { + state: { launchPaymentModalAutomatically: true }, + }, + ); + } + break; + } + case "resubscribe": { + const subscription = await this.organizationApiService.getSubscription(organization.id); + const dialogReference = openChangePlanDialog(this.dialogService, { + data: { + organizationId: organization.id, + subscription: subscription, + productTierType: organization.productTierType, + }, + }); + await lastValueFrom(dialogReference.closed); + break; + } + case "contact_owner": { + await this.dialogService.openSimpleDialog({ + title: this.i18nService.t("suspendedOrganizationTitle", organization.name), + content: { key: "suspendedUserOrgMessage" }, + type: "danger", + acceptButtonText: this.i18nService.t("close"), + cancelButtonText: null, + }); + break; + } + } + }), + ); + + private getResponse$ = (organization: Organization): Observable => { + const existing = this.cache$.get(organization.id as OrganizationId); + if (existing) { + return existing; + } + const response$ = from(this.organizationBillingApiService.getWarnings(organization.id)).pipe( + shareReplay({ bufferSize: 1, refCount: false }), + ); + this.cache$.set(organization.id as OrganizationId, response$); + return response$; + }; + + private getWarning$ = ( + organization: Organization, + extract: (response: OrganizationWarningsResponse) => T | null | undefined, + ): Observable => + this.getResponse$(organization).pipe( + map(extract), + takeWhile((warning): warning is T => !!warning), + take(1), + ); +} diff --git a/apps/web/src/app/billing/warnings/free-trial-warning.component.ts b/apps/web/src/app/billing/warnings/free-trial-warning.component.ts new file mode 100644 index 00000000000..e83873e9d6b --- /dev/null +++ b/apps/web/src/app/billing/warnings/free-trial-warning.component.ts @@ -0,0 +1,56 @@ +import { AsyncPipe } from "@angular/common"; +import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core"; +import { Observable } from "rxjs"; + +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AnchorLinkDirective, BannerComponent } from "@bitwarden/components"; +import { I18nPipe } from "@bitwarden/ui-common"; + +import { + FreeTrialWarning, + OrganizationWarningsService, +} from "../services/organization-warnings.service"; + +@Component({ + selector: "app-free-trial-warning", + template: ` + @let warning = freeTrialWarning$ | async; + + @if (warning) { + + {{ warning.message }} + + {{ "clickHereToAddPaymentMethod" | i18n }} + + + } + `, + standalone: true, + imports: [AnchorLinkDirective, AsyncPipe, BannerComponent, I18nPipe], +}) +export class FreeTrialWarningComponent implements OnInit { + @Input({ required: true }) organization!: Organization; + @Output() clicked = new EventEmitter(); + + freeTrialWarning$!: Observable; + + constructor(private organizationWarningsService: OrganizationWarningsService) {} + + ngOnInit() { + this.freeTrialWarning$ = this.organizationWarningsService.getFreeTrialWarning$( + this.organization, + ); + } +} diff --git a/apps/web/src/app/billing/warnings/reseller-renewal-warning.component.ts b/apps/web/src/app/billing/warnings/reseller-renewal-warning.component.ts new file mode 100644 index 00000000000..fc94e85e28d --- /dev/null +++ b/apps/web/src/app/billing/warnings/reseller-renewal-warning.component.ts @@ -0,0 +1,45 @@ +import { AsyncPipe } from "@angular/common"; +import { Component, Input, OnInit } from "@angular/core"; +import { Observable } from "rxjs"; + +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { BannerComponent } from "@bitwarden/components"; + +import { + OrganizationWarningsService, + ResellerRenewalWarning, +} from "../services/organization-warnings.service"; + +@Component({ + selector: "app-reseller-renewal-warning", + template: ` + @let warning = resellerRenewalWarning$ | async; + + @if (warning) { + + {{ warning.message }} + + } + `, + standalone: true, + imports: [AsyncPipe, BannerComponent], +}) +export class ResellerRenewalWarningComponent implements OnInit { + @Input({ required: true }) organization!: Organization; + + resellerRenewalWarning$!: Observable; + + constructor(private organizationWarningsService: OrganizationWarningsService) {} + + ngOnInit() { + this.resellerRenewalWarning$ = this.organizationWarningsService.getResellerRenewalWarning$( + this.organization, + ); + } +} diff --git a/libs/common/src/billing/abstractions/organizations/organization-billing-api.service.abstraction.ts b/libs/common/src/billing/abstractions/organizations/organization-billing-api.service.abstraction.ts index e1f7ad49012..4975da0d7d2 100644 --- a/libs/common/src/billing/abstractions/organizations/organization-billing-api.service.abstraction.ts +++ b/libs/common/src/billing/abstractions/organizations/organization-billing-api.service.abstraction.ts @@ -1,3 +1,5 @@ +import { OrganizationWarningsResponse } from "@bitwarden/common/billing/models/response/organization-warnings.response"; + import { BillingInvoiceResponse, BillingTransactionResponse, @@ -15,6 +17,8 @@ export abstract class OrganizationBillingApiServiceAbstraction { startAfter?: string, ) => Promise; + abstract getWarnings: (id: string) => Promise; + abstract setupBusinessUnit: ( id: string, request: { diff --git a/libs/common/src/billing/models/response/organization-warnings.response.ts b/libs/common/src/billing/models/response/organization-warnings.response.ts new file mode 100644 index 00000000000..ff70298101e --- /dev/null +++ b/libs/common/src/billing/models/response/organization-warnings.response.ts @@ -0,0 +1,97 @@ +import { BaseResponse } from "../../../models/response/base.response"; + +export class OrganizationWarningsResponse extends BaseResponse { + freeTrial?: FreeTrialWarningResponse; + inactiveSubscription?: InactiveSubscriptionWarningResponse; + resellerRenewal?: ResellerRenewalWarningResponse; + + constructor(response: any) { + super(response); + const freeTrialWarning = this.getResponseProperty("FreeTrial"); + if (freeTrialWarning) { + this.freeTrial = new FreeTrialWarningResponse(freeTrialWarning); + } + const inactiveSubscriptionWarning = this.getResponseProperty("InactiveSubscription"); + if (inactiveSubscriptionWarning) { + this.inactiveSubscription = new InactiveSubscriptionWarningResponse( + inactiveSubscriptionWarning, + ); + } + const resellerWarning = this.getResponseProperty("ResellerRenewal"); + if (resellerWarning) { + this.resellerRenewal = new ResellerRenewalWarningResponse(resellerWarning); + } + } +} + +class FreeTrialWarningResponse extends BaseResponse { + remainingTrialDays: number; + + constructor(response: any) { + super(response); + this.remainingTrialDays = this.getResponseProperty("RemainingTrialDays"); + } +} + +class InactiveSubscriptionWarningResponse extends BaseResponse { + resolution: string; + + constructor(response: any) { + super(response); + this.resolution = this.getResponseProperty("Resolution"); + } +} + +class ResellerRenewalWarningResponse extends BaseResponse { + type: "upcoming" | "issued" | "past_due"; + upcoming?: UpcomingRenewal; + issued?: IssuedRenewal; + pastDue?: PastDueRenewal; + + constructor(response: any) { + super(response); + this.type = this.getResponseProperty("Type"); + switch (this.type) { + case "upcoming": { + this.upcoming = new UpcomingRenewal(this.getResponseProperty("Upcoming")); + break; + } + case "issued": { + this.issued = new IssuedRenewal(this.getResponseProperty("Issued")); + break; + } + case "past_due": { + this.pastDue = new PastDueRenewal(this.getResponseProperty("PastDue")); + } + } + } +} + +class UpcomingRenewal extends BaseResponse { + renewalDate: Date; + + constructor(response: any) { + super(response); + this.renewalDate = new Date(this.getResponseProperty("RenewalDate")); + } +} + +class IssuedRenewal extends BaseResponse { + issuedDate: Date; + dueDate: Date; + + constructor(response: any) { + super(response); + this.issuedDate = new Date(this.getResponseProperty("IssuedDate")); + this.dueDate = new Date(this.getResponseProperty("DueDate")); + } +} + +class PastDueRenewal extends BaseResponse { + suspensionDate: Date; + + constructor(response: any) { + super(response); + this.suspensionDate = new Date(this.getResponseProperty("SuspensionDate")); + } +} diff --git a/libs/common/src/billing/services/organization/organization-billing-api.service.ts b/libs/common/src/billing/services/organization/organization-billing-api.service.ts index 405bd41957f..1189316a487 100644 --- a/libs/common/src/billing/services/organization/organization-billing-api.service.ts +++ b/libs/common/src/billing/services/organization/organization-billing-api.service.ts @@ -1,3 +1,5 @@ +import { OrganizationWarningsResponse } from "@bitwarden/common/billing/models/response/organization-warnings.response"; + import { ApiService } from "../../../abstractions/api.service"; import { OrganizationBillingApiServiceAbstraction } from "../../abstractions/organizations/organization-billing-api.service.abstraction"; import { @@ -50,6 +52,18 @@ export class OrganizationBillingApiService implements OrganizationBillingApiServ return r?.map((i: any) => new BillingTransactionResponse(i)) || []; } + async getWarnings(id: string): Promise { + const response = await this.apiService.send( + "GET", + `/organizations/${id}/billing/warnings`, + null, + true, + true, + ); + + return new OrganizationWarningsResponse(response); + } + async setupBusinessUnit( id: string, request: { diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index 3644ceefa9a..3e905a6253c 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -35,6 +35,7 @@ export enum FeatureFlag { PM18794_ProviderPaymentMethod = "pm-18794-provider-payment-method", PM17772_AdminInitiatedSponsorships = "pm-17772-admin-initiated-sponsorships", PM19956_RequireProviderPaymentMethodDuringSetup = "pm-19956-require-provider-payment-method-during-setup", + UseOrganizationWarningsService = "use-organization-warnings-service", /* Data Insights and Reporting */ CriticalApps = "pm-14466-risk-insights-critical-application", @@ -123,6 +124,7 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.PM18794_ProviderPaymentMethod]: FALSE, [FeatureFlag.PM17772_AdminInitiatedSponsorships]: FALSE, [FeatureFlag.PM19956_RequireProviderPaymentMethodDuringSetup]: FALSE, + [FeatureFlag.UseOrganizationWarningsService]: FALSE, /* Key Management */ [FeatureFlag.PrivateKeyRegeneration]: FALSE, From 835516a925304803416a703f81dede6b954f6ee3 Mon Sep 17 00:00:00 2001 From: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com> Date: Fri, 2 May 2025 08:26:48 +0200 Subject: [PATCH 114/499] [PM-20424] [BEEEP] browser client webpack optimisations (#14343) * browser client webpack optimisations * fixing browser extension loading --- apps/browser/webpack.config.js | 35 ++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/apps/browser/webpack.config.js b/apps/browser/webpack.config.js index 4b66ed7d70a..6d9113be7ed 100644 --- a/apps/browser/webpack.config.js +++ b/apps/browser/webpack.config.js @@ -81,6 +81,8 @@ const moduleRules = [ loader: "babel-loader", options: { configFile: "../../babel.config.json", + cacheDirectory: ENV === "development", + compact: ENV !== "development", }, }, ], @@ -205,6 +207,20 @@ const mainConfig = { "content/send-on-installed-message": "./src/vault/content/send-on-installed-message.ts", "content/send-popup-open-message": "./src/vault/content/send-popup-open-message.ts", }, + cache: + ENV !== "development" + ? false + : { + type: "filesystem", + name: "main-cache", + cacheDirectory: path.resolve(__dirname, "../../node_modules/.cache/webpack-browser-main"), + buildDependencies: { + config: [__filename], + }, + }, + snapshot: { + unmanagedPaths: [path.resolve(__dirname, "../../node_modules/@bitwarden/")], + }, optimization: { minimize: ENV !== "development", minimizer: [ @@ -263,6 +279,7 @@ const mainConfig = { fs: false, path: require.resolve("path-browserify"), }, + cache: true, }, output: { filename: "[name].js", @@ -357,6 +374,23 @@ if (manifestVersion == 2) { ], noParse: /argon2(-simd)?\.wasm$/, }, + cache: + ENV !== "development" + ? false + : { + type: "filesystem", + name: "background-cache", + cacheDirectory: path.resolve( + __dirname, + "../../node_modules/.cache/webpack-browser-background", + ), + buildDependencies: { + config: [__filename], + }, + }, + snapshot: { + unmanagedPaths: [path.resolve(__dirname, "../../node_modules/@bitwarden/")], + }, experiments: { asyncWebAssembly: true, }, @@ -369,6 +403,7 @@ if (manifestVersion == 2) { fs: false, path: require.resolve("path-browserify"), }, + cache: true, }, dependencies: ["main"], plugins: [...requiredPlugins], From 2ba022532c7f7a48fb3161a10dd0fae1ff5e675e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 2 May 2025 13:20:46 +0200 Subject: [PATCH 115/499] [deps] Architecture: Update eslint-import-resolver-typescript to v4 (#14509) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 23 ++++++----------------- package.json | 2 +- 2 files changed, 7 insertions(+), 18 deletions(-) diff --git a/package-lock.json b/package-lock.json index 25322b844b7..95e44955767 100644 --- a/package-lock.json +++ b/package-lock.json @@ -139,7 +139,7 @@ "electron-updater": "6.3.9", "eslint": "8.57.1", "eslint-config-prettier": "10.1.2", - "eslint-import-resolver-typescript": "3.10.1", + "eslint-import-resolver-typescript": "4.3.4", "eslint-plugin-import": "2.31.0", "eslint-plugin-rxjs": "5.0.3", "eslint-plugin-rxjs-angular": "2.0.1", @@ -8336,16 +8336,6 @@ "node": ">= 8" } }, - "node_modules/@nolyfill/is-core-module": { - "version": "1.0.39", - "resolved": "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz", - "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.4.0" - } - }, "node_modules/@npmcli/agent": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-2.2.2.tgz", @@ -19053,22 +19043,21 @@ } }, "node_modules/eslint-import-resolver-typescript": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.10.1.tgz", - "integrity": "sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-4.3.4.tgz", + "integrity": "sha512-buzw5z5VtiQMysYLH9iW9BV04YyZebsw+gPi+c4FCjfS9i6COYOrEWw9t3m3wA9PFBfqcBCqWf32qrXLbwafDw==", "dev": true, "license": "ISC", "dependencies": { - "@nolyfill/is-core-module": "1.0.39", "debug": "^4.4.0", "get-tsconfig": "^4.10.0", "is-bun-module": "^2.0.0", "stable-hash": "^0.0.5", "tinyglobby": "^0.2.13", - "unrs-resolver": "^1.6.2" + "unrs-resolver": "^1.6.3" }, "engines": { - "node": "^14.18.0 || >=16.0.0" + "node": "^16.17.0 || >=18.6.0" }, "funding": { "url": "https://opencollective.com/eslint-import-resolver-typescript" diff --git a/package.json b/package.json index bc00ac57a59..0060cf56d4b 100644 --- a/package.json +++ b/package.json @@ -101,7 +101,7 @@ "electron-updater": "6.3.9", "eslint": "8.57.1", "eslint-config-prettier": "10.1.2", - "eslint-import-resolver-typescript": "3.10.1", + "eslint-import-resolver-typescript": "4.3.4", "eslint-plugin-import": "2.31.0", "eslint-plugin-rxjs": "5.0.3", "eslint-plugin-rxjs-angular": "2.0.1", From 534c2be55b0a24d1a3b6e7f1401107edd428f6fa Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 2 May 2025 13:52:55 +0200 Subject: [PATCH 116/499] [deps]: Update Linting minor-patch to v8.31.0 (#14583) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Oscar Hinton --- package-lock.json | 507 +++++++--------------------------------------- package.json | 6 +- 2 files changed, 76 insertions(+), 437 deletions(-) diff --git a/package-lock.json b/package-lock.json index 95e44955767..d7895d5a272 100644 --- a/package-lock.json +++ b/package-lock.json @@ -117,8 +117,8 @@ "@types/proper-lockfile": "4.1.4", "@types/retry": "0.12.5", "@types/zxcvbn": "4.4.5", - "@typescript-eslint/rule-tester": "8.22.0", - "@typescript-eslint/utils": "8.22.0", + "@typescript-eslint/rule-tester": "8.31.0", + "@typescript-eslint/utils": "8.31.0", "@webcomponents/custom-elements": "1.6.0", "@yao-pkg/pkg": "5.16.1", "angular-eslint": "18.4.3", @@ -174,7 +174,7 @@ "tsconfig-paths-webpack-plugin": "4.2.0", "type-fest": "2.19.0", "typescript": "5.4.2", - "typescript-eslint": "8.30.1", + "typescript-eslint": "8.31.0", "typescript-strict-plugin": "2.4.4", "url": "0.11.4", "util": "0.12.5", @@ -11951,17 +11951,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.30.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.30.1.tgz", - "integrity": "sha512-v+VWphxMjn+1t48/jO4t950D6KR8JaJuNXzi33Ve6P8sEmPr5k6CEXjdGwT6+LodVnEa91EQCtwjWNUCPweo+Q==", + "version": "8.31.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.31.0.tgz", + "integrity": "sha512-evaQJZ/J/S4wisevDvC1KFZkPzRetH8kYZbkgcTRyql3mcKsf+ZFDV1BVWUGTCAW5pQHoqn5gK5b8kn7ou9aFQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.30.1", - "@typescript-eslint/type-utils": "8.30.1", - "@typescript-eslint/utils": "8.30.1", - "@typescript-eslint/visitor-keys": "8.30.1", + "@typescript-eslint/scope-manager": "8.31.0", + "@typescript-eslint/type-utils": "8.31.0", + "@typescript-eslint/utils": "8.31.0", + "@typescript-eslint/visitor-keys": "8.31.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -11980,106 +11980,6 @@ "typescript": ">=4.8.4 <5.9.0" } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/scope-manager": { - "version": "8.30.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.30.1.tgz", - "integrity": "sha512-+C0B6ChFXZkuaNDl73FJxRYT0G7ufVPOSQkqkpM/U198wUwUFOtgo1k/QzFh1KjpBitaK7R1tgjVz6o9HmsRPg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.30.1", - "@typescript-eslint/visitor-keys": "8.30.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/typescript-estree": { - "version": "8.30.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.30.1.tgz", - "integrity": "sha512-kQQnxymiUy9tTb1F2uep9W6aBiYODgq5EMSk6Nxh4Z+BDUoYUSa029ISs5zTzKBFnexQEh71KqwjKnRz58lusQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.30.1", - "@typescript-eslint/visitor-keys": "8.30.1", - "debug": "^4.3.4", - "fast-glob": "^3.3.2", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^2.0.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": { - "version": "8.30.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.30.1.tgz", - "integrity": "sha512-T/8q4R9En2tcEsWPQgB5BQ0XJVOtfARcUvOa8yJP3fh9M/mXraLxZrkCfGb6ChrO/V3W+Xbd04RacUEqk1CFEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.30.1", - "@typescript-eslint/types": "8.30.1", - "@typescript-eslint/typescript-estree": "8.30.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/visitor-keys": { - "version": "8.30.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.30.1.tgz", - "integrity": "sha512-aEhgas7aJ6vZnNFC7K4/vMGDGyOiqWcYZPpIWrTKuTAlsvDNKy2GFDqh9smL+iq069ZvR0YzEeq0B8NJlLzjFA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.30.1", - "eslint-visitor-keys": "^4.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -12281,16 +12181,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.30.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.30.1.tgz", - "integrity": "sha512-H+vqmWwT5xoNrXqWs/fesmssOW70gxFlgcMlYcBaWNPIEWDgLa4W9nkSPmhuOgLnXq9QYgkZ31fhDyLhleCsAg==", + "version": "8.31.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.31.0.tgz", + "integrity": "sha512-67kYYShjBR0jNI5vsf/c3WG4u+zDnCTHTPqVMQguffaWWFs7artgwKmfwdifl+r6XyM5LYLas/dInj2T0SgJyw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.30.1", - "@typescript-eslint/types": "8.30.1", - "@typescript-eslint/typescript-estree": "8.30.1", - "@typescript-eslint/visitor-keys": "8.30.1", + "@typescript-eslint/scope-manager": "8.31.0", + "@typescript-eslint/types": "8.31.0", + "@typescript-eslint/typescript-estree": "8.31.0", + "@typescript-eslint/visitor-keys": "8.31.0", "debug": "^4.3.4" }, "engines": { @@ -12305,16 +12205,12 @@ "typescript": ">=4.8.4 <5.9.0" } }, - "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/scope-manager": { - "version": "8.30.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.30.1.tgz", - "integrity": "sha512-+C0B6ChFXZkuaNDl73FJxRYT0G7ufVPOSQkqkpM/U198wUwUFOtgo1k/QzFh1KjpBitaK7R1tgjVz6o9HmsRPg==", + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/types": { + "version": "8.31.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.31.0.tgz", + "integrity": "sha512-Ch8oSjVyYyJxPQk8pMiP2FFGYatqXQfQIaMp+TpuuLlDachRWpUAeEu1u9B/v/8LToehUIWyiKcA/w5hUFRKuQ==", "dev": true, "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.30.1", - "@typescript-eslint/visitor-keys": "8.30.1" - }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -12323,73 +12219,16 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": { - "version": "8.30.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.30.1.tgz", - "integrity": "sha512-kQQnxymiUy9tTb1F2uep9W6aBiYODgq5EMSk6Nxh4Z+BDUoYUSa029ISs5zTzKBFnexQEh71KqwjKnRz58lusQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.30.1", - "@typescript-eslint/visitor-keys": "8.30.1", - "debug": "^4.3.4", - "fast-glob": "^3.3.2", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^2.0.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/visitor-keys": { - "version": "8.30.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.30.1.tgz", - "integrity": "sha512-aEhgas7aJ6vZnNFC7K4/vMGDGyOiqWcYZPpIWrTKuTAlsvDNKy2GFDqh9smL+iq069ZvR0YzEeq0B8NJlLzjFA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.30.1", - "eslint-visitor-keys": "^4.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/parser/node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, "node_modules/@typescript-eslint/rule-tester": { - "version": "8.22.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/rule-tester/-/rule-tester-8.22.0.tgz", - "integrity": "sha512-krTIaDM08bSQ9sIpqDTP0aX4P0Ck/WQpj+7uMIeNqzzWEWmoJFyle12B0Na15KwBLPV2MJPmaZUn+v2qenXjaw==", + "version": "8.31.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/rule-tester/-/rule-tester-8.31.0.tgz", + "integrity": "sha512-17Dn3vpNtHqxyjOieODNKBAyxH8WNvkfZAzkHR2VA0cW/49YcmHF3YyenNI3wfF05iZJ9MWnjU2kBGz0gDavfw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.22.0", - "@typescript-eslint/utils": "8.22.0", + "@typescript-eslint/parser": "8.31.0", + "@typescript-eslint/typescript-estree": "8.31.0", + "@typescript-eslint/utils": "8.31.0", "ajv": "^6.12.6", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "4.6.2", @@ -12431,14 +12270,14 @@ "license": "MIT" }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.22.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.22.0.tgz", - "integrity": "sha512-/lwVV0UYgkj7wPSw0o8URy6YI64QmcOdwHuGuxWIYznO6d45ER0wXUbksr9pYdViAofpUCNJx/tAzNukgvaaiQ==", + "version": "8.31.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.31.0.tgz", + "integrity": "sha512-knO8UyF78Nt8O/B64i7TlGXod69ko7z6vJD9uhSlm0qkAbGeRUSudcm0+K/4CrRjrpiHfBCjMWlc08Vav1xwcw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.22.0", - "@typescript-eslint/visitor-keys": "8.22.0" + "@typescript-eslint/types": "8.31.0", + "@typescript-eslint/visitor-keys": "8.31.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -12449,9 +12288,9 @@ } }, "node_modules/@typescript-eslint/scope-manager/node_modules/@typescript-eslint/types": { - "version": "8.22.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.22.0.tgz", - "integrity": "sha512-0S4M4baNzp612zwpD4YOieP3VowOARgK2EkN/GBn95hpyF8E2fbMT55sRHWBq+Huaqk3b3XK+rxxlM8sPgGM6A==", + "version": "8.31.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.31.0.tgz", + "integrity": "sha512-Ch8oSjVyYyJxPQk8pMiP2FFGYatqXQfQIaMp+TpuuLlDachRWpUAeEu1u9B/v/8LToehUIWyiKcA/w5hUFRKuQ==", "dev": true, "license": "MIT", "engines": { @@ -12463,14 +12302,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.30.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.30.1.tgz", - "integrity": "sha512-64uBF76bfQiJyHgZISC7vcNz3adqQKIccVoKubyQcOnNcdJBvYOILV1v22Qhsw3tw3VQu5ll8ND6hycgAR5fEA==", + "version": "8.31.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.31.0.tgz", + "integrity": "sha512-DJ1N1GdjI7IS7uRlzJuEDCgDQix3ZVYVtgeWEyhyn4iaoitpMBX6Ndd488mXSx0xah/cONAkEaYyylDyAeHMHg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.30.1", - "@typescript-eslint/utils": "8.30.1", + "@typescript-eslint/typescript-estree": "8.31.0", + "@typescript-eslint/utils": "8.31.0", "debug": "^4.3.4", "ts-api-utils": "^2.0.1" }, @@ -12486,106 +12325,6 @@ "typescript": ">=4.8.4 <5.9.0" } }, - "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/scope-manager": { - "version": "8.30.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.30.1.tgz", - "integrity": "sha512-+C0B6ChFXZkuaNDl73FJxRYT0G7ufVPOSQkqkpM/U198wUwUFOtgo1k/QzFh1KjpBitaK7R1tgjVz6o9HmsRPg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.30.1", - "@typescript-eslint/visitor-keys": "8.30.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree": { - "version": "8.30.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.30.1.tgz", - "integrity": "sha512-kQQnxymiUy9tTb1F2uep9W6aBiYODgq5EMSk6Nxh4Z+BDUoYUSa029ISs5zTzKBFnexQEh71KqwjKnRz58lusQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.30.1", - "@typescript-eslint/visitor-keys": "8.30.1", - "debug": "^4.3.4", - "fast-glob": "^3.3.2", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^2.0.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/utils": { - "version": "8.30.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.30.1.tgz", - "integrity": "sha512-T/8q4R9En2tcEsWPQgB5BQ0XJVOtfARcUvOa8yJP3fh9M/mXraLxZrkCfGb6ChrO/V3W+Xbd04RacUEqk1CFEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.30.1", - "@typescript-eslint/types": "8.30.1", - "@typescript-eslint/typescript-estree": "8.30.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/visitor-keys": { - "version": "8.30.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.30.1.tgz", - "integrity": "sha512-aEhgas7aJ6vZnNFC7K4/vMGDGyOiqWcYZPpIWrTKuTAlsvDNKy2GFDqh9smL+iq069ZvR0YzEeq0B8NJlLzjFA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.30.1", - "eslint-visitor-keys": "^4.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/type-utils/node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, "node_modules/@typescript-eslint/types": { "version": "8.30.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.30.1.tgz", @@ -12601,20 +12340,20 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.22.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.22.0.tgz", - "integrity": "sha512-SJX99NAS2ugGOzpyhMza/tX+zDwjvwAtQFLsBo3GQxiGcvaKlqGBkmZ+Y1IdiSi9h4Q0Lr5ey+Cp9CGWNY/F/w==", + "version": "8.31.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.31.0.tgz", + "integrity": "sha512-xLmgn4Yl46xi6aDSZ9KkyfhhtnYI15/CvHbpOy/eR5NWhK/BK8wc709KKwhAR0m4ZKRP7h07bm4BWUYOCuRpQQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.22.0", - "@typescript-eslint/visitor-keys": "8.22.0", + "@typescript-eslint/types": "8.31.0", + "@typescript-eslint/visitor-keys": "8.31.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", - "ts-api-utils": "^2.0.0" + "ts-api-utils": "^2.0.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -12624,13 +12363,13 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <5.8.0" + "typescript": ">=4.8.4 <5.9.0" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/types": { - "version": "8.22.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.22.0.tgz", - "integrity": "sha512-0S4M4baNzp612zwpD4YOieP3VowOARgK2EkN/GBn95hpyF8E2fbMT55sRHWBq+Huaqk3b3XK+rxxlM8sPgGM6A==", + "version": "8.31.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.31.0.tgz", + "integrity": "sha512-Ch8oSjVyYyJxPQk8pMiP2FFGYatqXQfQIaMp+TpuuLlDachRWpUAeEu1u9B/v/8LToehUIWyiKcA/w5hUFRKuQ==", "dev": true, "license": "MIT", "engines": { @@ -12642,16 +12381,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.22.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.22.0.tgz", - "integrity": "sha512-T8oc1MbF8L+Bk2msAvCUzjxVB2Z2f+vXYfcucE2wOmYs7ZUwco5Ep0fYZw8quNwOiw9K8GYVL+Kgc2pETNTLOg==", + "version": "8.31.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.31.0.tgz", + "integrity": "sha512-qi6uPLt9cjTFxAb1zGNgTob4x9ur7xC6mHQJ8GwEzGMGE9tYniublmJaowOJ9V2jUzxrltTPfdG2nKlWsq0+Ww==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.22.0", - "@typescript-eslint/types": "8.22.0", - "@typescript-eslint/typescript-estree": "8.22.0" + "@typescript-eslint/scope-manager": "8.31.0", + "@typescript-eslint/types": "8.31.0", + "@typescript-eslint/typescript-estree": "8.31.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -12662,13 +12401,13 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.8.0" + "typescript": ">=4.8.4 <5.9.0" } }, "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/types": { - "version": "8.22.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.22.0.tgz", - "integrity": "sha512-0S4M4baNzp612zwpD4YOieP3VowOARgK2EkN/GBn95hpyF8E2fbMT55sRHWBq+Huaqk3b3XK+rxxlM8sPgGM6A==", + "version": "8.31.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.31.0.tgz", + "integrity": "sha512-Ch8oSjVyYyJxPQk8pMiP2FFGYatqXQfQIaMp+TpuuLlDachRWpUAeEu1u9B/v/8LToehUIWyiKcA/w5hUFRKuQ==", "dev": true, "license": "MIT", "engines": { @@ -12680,13 +12419,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.22.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.22.0.tgz", - "integrity": "sha512-AWpYAXnUgvLNabGTy3uBylkgZoosva/miNd1I8Bz3SjotmQPbVqhO4Cczo8AsZ44XVErEBPr/CRSgaj8sG7g0w==", + "version": "8.31.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.31.0.tgz", + "integrity": "sha512-QcGHmlRHWOl93o64ZUMNewCdwKGU6WItOU52H0djgNmn1EOrhVudrDzXz4OycCRSCPwFCDrE2iIt5vmuUdHxuQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.22.0", + "@typescript-eslint/types": "8.31.0", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -12698,9 +12437,9 @@ } }, "node_modules/@typescript-eslint/visitor-keys/node_modules/@typescript-eslint/types": { - "version": "8.22.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.22.0.tgz", - "integrity": "sha512-0S4M4baNzp612zwpD4YOieP3VowOARgK2EkN/GBn95hpyF8E2fbMT55sRHWBq+Huaqk3b3XK+rxxlM8sPgGM6A==", + "version": "8.31.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.31.0.tgz", + "integrity": "sha512-Ch8oSjVyYyJxPQk8pMiP2FFGYatqXQfQIaMp+TpuuLlDachRWpUAeEu1u9B/v/8LToehUIWyiKcA/w5hUFRKuQ==", "dev": true, "license": "MIT", "engines": { @@ -35407,15 +35146,15 @@ } }, "node_modules/typescript-eslint": { - "version": "8.30.1", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.30.1.tgz", - "integrity": "sha512-D7lC0kcehVH7Mb26MRQi64LMyRJsj3dToJxM1+JVTl53DQSV5/7oUGWQLcKl1C1KnoVHxMMU2FNQMffr7F3Row==", + "version": "8.31.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.31.0.tgz", + "integrity": "sha512-u+93F0sB0An8WEAPtwxVhFby573E8ckdjwUUQUj9QA4v8JAvgtoDdIyYR3XFwFHq2W1KJ1AurwJCO+w+Y1ixyQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.30.1", - "@typescript-eslint/parser": "8.30.1", - "@typescript-eslint/utils": "8.30.1" + "@typescript-eslint/eslint-plugin": "8.31.0", + "@typescript-eslint/parser": "8.31.0", + "@typescript-eslint/utils": "8.31.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -35429,106 +35168,6 @@ "typescript": ">=4.8.4 <5.9.0" } }, - "node_modules/typescript-eslint/node_modules/@typescript-eslint/scope-manager": { - "version": "8.30.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.30.1.tgz", - "integrity": "sha512-+C0B6ChFXZkuaNDl73FJxRYT0G7ufVPOSQkqkpM/U198wUwUFOtgo1k/QzFh1KjpBitaK7R1tgjVz6o9HmsRPg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.30.1", - "@typescript-eslint/visitor-keys": "8.30.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/typescript-eslint/node_modules/@typescript-eslint/typescript-estree": { - "version": "8.30.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.30.1.tgz", - "integrity": "sha512-kQQnxymiUy9tTb1F2uep9W6aBiYODgq5EMSk6Nxh4Z+BDUoYUSa029ISs5zTzKBFnexQEh71KqwjKnRz58lusQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.30.1", - "@typescript-eslint/visitor-keys": "8.30.1", - "debug": "^4.3.4", - "fast-glob": "^3.3.2", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^2.0.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/typescript-eslint/node_modules/@typescript-eslint/utils": { - "version": "8.30.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.30.1.tgz", - "integrity": "sha512-T/8q4R9En2tcEsWPQgB5BQ0XJVOtfARcUvOa8yJP3fh9M/mXraLxZrkCfGb6ChrO/V3W+Xbd04RacUEqk1CFEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.30.1", - "@typescript-eslint/types": "8.30.1", - "@typescript-eslint/typescript-estree": "8.30.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/typescript-eslint/node_modules/@typescript-eslint/visitor-keys": { - "version": "8.30.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.30.1.tgz", - "integrity": "sha512-aEhgas7aJ6vZnNFC7K4/vMGDGyOiqWcYZPpIWrTKuTAlsvDNKy2GFDqh9smL+iq069ZvR0YzEeq0B8NJlLzjFA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.30.1", - "eslint-visitor-keys": "^4.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/typescript-eslint/node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, "node_modules/typescript-strict-plugin": { "version": "2.4.4", "resolved": "https://registry.npmjs.org/typescript-strict-plugin/-/typescript-strict-plugin-2.4.4.tgz", diff --git a/package.json b/package.json index 0060cf56d4b..f7cb09c451c 100644 --- a/package.json +++ b/package.json @@ -79,8 +79,8 @@ "@types/proper-lockfile": "4.1.4", "@types/retry": "0.12.5", "@types/zxcvbn": "4.4.5", - "@typescript-eslint/rule-tester": "8.22.0", - "@typescript-eslint/utils": "8.22.0", + "@typescript-eslint/rule-tester": "8.31.0", + "@typescript-eslint/utils": "8.31.0", "@webcomponents/custom-elements": "1.6.0", "@yao-pkg/pkg": "5.16.1", "angular-eslint": "18.4.3", @@ -136,7 +136,7 @@ "tsconfig-paths-webpack-plugin": "4.2.0", "type-fest": "2.19.0", "typescript": "5.4.2", - "typescript-eslint": "8.30.1", + "typescript-eslint": "8.31.0", "typescript-strict-plugin": "2.4.4", "url": "0.11.4", "util": "0.12.5", From 1e663b0941b9f4ad5e7b6fa63a5ca3a4a433be89 Mon Sep 17 00:00:00 2001 From: "bw-ghapp[bot]" <178206702+bw-ghapp[bot]@users.noreply.github.com> Date: Fri, 2 May 2025 14:17:30 +0200 Subject: [PATCH 117/499] Autosync the updated translations (#14593) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/desktop/src/locales/af/messages.json | 30 +++++ apps/desktop/src/locales/ar/messages.json | 30 +++++ apps/desktop/src/locales/az/messages.json | 30 +++++ apps/desktop/src/locales/be/messages.json | 30 +++++ apps/desktop/src/locales/bg/messages.json | 30 +++++ apps/desktop/src/locales/bn/messages.json | 30 +++++ apps/desktop/src/locales/bs/messages.json | 30 +++++ apps/desktop/src/locales/ca/messages.json | 30 +++++ apps/desktop/src/locales/cs/messages.json | 30 +++++ apps/desktop/src/locales/cy/messages.json | 30 +++++ apps/desktop/src/locales/da/messages.json | 30 +++++ apps/desktop/src/locales/de/messages.json | 30 +++++ apps/desktop/src/locales/el/messages.json | 30 +++++ apps/desktop/src/locales/en_GB/messages.json | 30 +++++ apps/desktop/src/locales/en_IN/messages.json | 30 +++++ apps/desktop/src/locales/eo/messages.json | 30 +++++ apps/desktop/src/locales/es/messages.json | 30 +++++ apps/desktop/src/locales/et/messages.json | 30 +++++ apps/desktop/src/locales/eu/messages.json | 30 +++++ apps/desktop/src/locales/fa/messages.json | 30 +++++ apps/desktop/src/locales/fi/messages.json | 112 +++++++++++------- apps/desktop/src/locales/fil/messages.json | 30 +++++ apps/desktop/src/locales/fr/messages.json | 30 +++++ apps/desktop/src/locales/gl/messages.json | 30 +++++ apps/desktop/src/locales/he/messages.json | 30 +++++ apps/desktop/src/locales/hi/messages.json | 30 +++++ apps/desktop/src/locales/hr/messages.json | 30 +++++ apps/desktop/src/locales/hu/messages.json | 30 +++++ apps/desktop/src/locales/id/messages.json | 30 +++++ apps/desktop/src/locales/it/messages.json | 30 +++++ apps/desktop/src/locales/ja/messages.json | 30 +++++ apps/desktop/src/locales/ka/messages.json | 30 +++++ apps/desktop/src/locales/km/messages.json | 30 +++++ apps/desktop/src/locales/kn/messages.json | 30 +++++ apps/desktop/src/locales/ko/messages.json | 30 +++++ apps/desktop/src/locales/lt/messages.json | 30 +++++ apps/desktop/src/locales/lv/messages.json | 40 ++++++- apps/desktop/src/locales/me/messages.json | 30 +++++ apps/desktop/src/locales/ml/messages.json | 30 +++++ apps/desktop/src/locales/mr/messages.json | 30 +++++ apps/desktop/src/locales/my/messages.json | 30 +++++ apps/desktop/src/locales/nb/messages.json | 30 +++++ apps/desktop/src/locales/ne/messages.json | 30 +++++ apps/desktop/src/locales/nl/messages.json | 30 +++++ apps/desktop/src/locales/nn/messages.json | 30 +++++ apps/desktop/src/locales/or/messages.json | 30 +++++ apps/desktop/src/locales/pl/messages.json | 30 +++++ apps/desktop/src/locales/pt_BR/messages.json | 30 +++++ apps/desktop/src/locales/pt_PT/messages.json | 30 +++++ apps/desktop/src/locales/ro/messages.json | 30 +++++ apps/desktop/src/locales/ru/messages.json | 30 +++++ apps/desktop/src/locales/si/messages.json | 30 +++++ apps/desktop/src/locales/sk/messages.json | 30 +++++ apps/desktop/src/locales/sl/messages.json | 30 +++++ apps/desktop/src/locales/sr/messages.json | 116 ++++++++++++------- apps/desktop/src/locales/sv/messages.json | 30 +++++ apps/desktop/src/locales/te/messages.json | 30 +++++ apps/desktop/src/locales/th/messages.json | 30 +++++ apps/desktop/src/locales/tr/messages.json | 30 +++++ apps/desktop/src/locales/uk/messages.json | 30 +++++ apps/desktop/src/locales/vi/messages.json | 30 +++++ apps/desktop/src/locales/zh_CN/messages.json | 34 +++++- apps/desktop/src/locales/zh_TW/messages.json | 30 +++++ 63 files changed, 1981 insertions(+), 91 deletions(-) diff --git a/apps/desktop/src/locales/af/messages.json b/apps/desktop/src/locales/af/messages.json index c75b1640ceb..f1c510893ef 100644 --- a/apps/desktop/src/locales/af/messages.json +++ b/apps/desktop/src/locales/af/messages.json @@ -3712,5 +3712,35 @@ }, "move": { "message": "Move" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } } diff --git a/apps/desktop/src/locales/ar/messages.json b/apps/desktop/src/locales/ar/messages.json index cca70b3c956..696bb21087f 100644 --- a/apps/desktop/src/locales/ar/messages.json +++ b/apps/desktop/src/locales/ar/messages.json @@ -3712,5 +3712,35 @@ }, "move": { "message": "Move" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } } diff --git a/apps/desktop/src/locales/az/messages.json b/apps/desktop/src/locales/az/messages.json index 9b10b7bdeeb..b3c61e0af43 100644 --- a/apps/desktop/src/locales/az/messages.json +++ b/apps/desktop/src/locales/az/messages.json @@ -3712,5 +3712,35 @@ }, "move": { "message": "Daşı" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } } diff --git a/apps/desktop/src/locales/be/messages.json b/apps/desktop/src/locales/be/messages.json index b6714f1d440..c01caa5c0ba 100644 --- a/apps/desktop/src/locales/be/messages.json +++ b/apps/desktop/src/locales/be/messages.json @@ -3712,5 +3712,35 @@ }, "move": { "message": "Move" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } } diff --git a/apps/desktop/src/locales/bg/messages.json b/apps/desktop/src/locales/bg/messages.json index 5cef611de63..1db8f182818 100644 --- a/apps/desktop/src/locales/bg/messages.json +++ b/apps/desktop/src/locales/bg/messages.json @@ -3712,5 +3712,35 @@ }, "move": { "message": "Преместване" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } } diff --git a/apps/desktop/src/locales/bn/messages.json b/apps/desktop/src/locales/bn/messages.json index d6e16c352ff..f41dcb12057 100644 --- a/apps/desktop/src/locales/bn/messages.json +++ b/apps/desktop/src/locales/bn/messages.json @@ -3712,5 +3712,35 @@ }, "move": { "message": "Move" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } } diff --git a/apps/desktop/src/locales/bs/messages.json b/apps/desktop/src/locales/bs/messages.json index fa62597873c..3188a1fa889 100644 --- a/apps/desktop/src/locales/bs/messages.json +++ b/apps/desktop/src/locales/bs/messages.json @@ -3712,5 +3712,35 @@ }, "move": { "message": "Move" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } } diff --git a/apps/desktop/src/locales/ca/messages.json b/apps/desktop/src/locales/ca/messages.json index 9c842b710d7..f989d530b2d 100644 --- a/apps/desktop/src/locales/ca/messages.json +++ b/apps/desktop/src/locales/ca/messages.json @@ -3712,5 +3712,35 @@ }, "move": { "message": "Move" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } } diff --git a/apps/desktop/src/locales/cs/messages.json b/apps/desktop/src/locales/cs/messages.json index cfa18447bc9..fba5cf3e1ce 100644 --- a/apps/desktop/src/locales/cs/messages.json +++ b/apps/desktop/src/locales/cs/messages.json @@ -3712,5 +3712,35 @@ }, "move": { "message": "Přesunout" + }, + "newLoginNudgeTitle": { + "message": "Ušetřete čas s automatickým vyplňováním" + }, + "newLoginNudgeBody": { + "message": "Zahrne webovou stránku, takže se toto přihlášení objeví jako návrh automatického vyplňování." + }, + "newCardNudgeTitle": { + "message": "Bezproblémová online pokladna" + }, + "newCardNudgeBody": { + "message": "Karty - snadné, bezpečné a přesné vyplňování platebních formulářů." + }, + "newIdentityNudgeTitle": { + "message": "Jednodušší vytváření účtů" + }, + "newIdentityNudgeBody": { + "message": "Identity - rychlé automatické vyplňování dlouhých registračních nebo kontaktních formulářů." + }, + "newNoteNudgeTitle": { + "message": "Udržujte svá citlivá data v bezpečí" + }, + "newNoteNudgeBody": { + "message": "Poznámky - bezpečné ukládání citlivých údajů, jako jsou bankovní nebo pojišťovací údaje." + }, + "newSshNudgeTitle": { + "message": "Přístup SSH pro vývojáře" + }, + "newSshNudgeBody": { + "message": "Uložte své klíče a připojte se k SSH agentovi pro rychlé a šifrované ověření." } } diff --git a/apps/desktop/src/locales/cy/messages.json b/apps/desktop/src/locales/cy/messages.json index 6c6a88be50d..efcf558bbed 100644 --- a/apps/desktop/src/locales/cy/messages.json +++ b/apps/desktop/src/locales/cy/messages.json @@ -3712,5 +3712,35 @@ }, "move": { "message": "Move" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } } diff --git a/apps/desktop/src/locales/da/messages.json b/apps/desktop/src/locales/da/messages.json index 6378f8106bb..f7b0c33707a 100644 --- a/apps/desktop/src/locales/da/messages.json +++ b/apps/desktop/src/locales/da/messages.json @@ -3712,5 +3712,35 @@ }, "move": { "message": "Move" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } } diff --git a/apps/desktop/src/locales/de/messages.json b/apps/desktop/src/locales/de/messages.json index c984397dfe0..b1c129fd488 100644 --- a/apps/desktop/src/locales/de/messages.json +++ b/apps/desktop/src/locales/de/messages.json @@ -3712,5 +3712,35 @@ }, "move": { "message": "Verschieben" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } } diff --git a/apps/desktop/src/locales/el/messages.json b/apps/desktop/src/locales/el/messages.json index a0045e2a47b..a91b678a04c 100644 --- a/apps/desktop/src/locales/el/messages.json +++ b/apps/desktop/src/locales/el/messages.json @@ -3712,5 +3712,35 @@ }, "move": { "message": "Move" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } } diff --git a/apps/desktop/src/locales/en_GB/messages.json b/apps/desktop/src/locales/en_GB/messages.json index 5262047789a..bcc9dd74934 100644 --- a/apps/desktop/src/locales/en_GB/messages.json +++ b/apps/desktop/src/locales/en_GB/messages.json @@ -3712,5 +3712,35 @@ }, "move": { "message": "Move" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } } diff --git a/apps/desktop/src/locales/en_IN/messages.json b/apps/desktop/src/locales/en_IN/messages.json index a3614289e16..8e40b110992 100644 --- a/apps/desktop/src/locales/en_IN/messages.json +++ b/apps/desktop/src/locales/en_IN/messages.json @@ -3712,5 +3712,35 @@ }, "move": { "message": "Move" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } } diff --git a/apps/desktop/src/locales/eo/messages.json b/apps/desktop/src/locales/eo/messages.json index c79e7e29af4..54322042979 100644 --- a/apps/desktop/src/locales/eo/messages.json +++ b/apps/desktop/src/locales/eo/messages.json @@ -3712,5 +3712,35 @@ }, "move": { "message": "Move" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } } diff --git a/apps/desktop/src/locales/es/messages.json b/apps/desktop/src/locales/es/messages.json index 9ba2d313b30..823d0cc6a2c 100644 --- a/apps/desktop/src/locales/es/messages.json +++ b/apps/desktop/src/locales/es/messages.json @@ -3712,5 +3712,35 @@ }, "move": { "message": "Mover" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } } diff --git a/apps/desktop/src/locales/et/messages.json b/apps/desktop/src/locales/et/messages.json index 615e52f98de..e2fb62c4df2 100644 --- a/apps/desktop/src/locales/et/messages.json +++ b/apps/desktop/src/locales/et/messages.json @@ -3712,5 +3712,35 @@ }, "move": { "message": "Move" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } } diff --git a/apps/desktop/src/locales/eu/messages.json b/apps/desktop/src/locales/eu/messages.json index f14fd0064d6..cfab30889eb 100644 --- a/apps/desktop/src/locales/eu/messages.json +++ b/apps/desktop/src/locales/eu/messages.json @@ -3712,5 +3712,35 @@ }, "move": { "message": "Move" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } } diff --git a/apps/desktop/src/locales/fa/messages.json b/apps/desktop/src/locales/fa/messages.json index f27a9f294cb..2fcb63b847b 100644 --- a/apps/desktop/src/locales/fa/messages.json +++ b/apps/desktop/src/locales/fa/messages.json @@ -3712,5 +3712,35 @@ }, "move": { "message": "Move" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } } diff --git a/apps/desktop/src/locales/fi/messages.json b/apps/desktop/src/locales/fi/messages.json index 77f22346ed5..627b65bb4ed 100644 --- a/apps/desktop/src/locales/fi/messages.json +++ b/apps/desktop/src/locales/fi/messages.json @@ -394,16 +394,16 @@ "message": "Todennusavain (TOTP)" }, "authenticatorKey": { - "message": "Authenticator key" + "message": "Todennusavain" }, "autofillOptions": { - "message": "Autofill options" + "message": "Automaattitäytön asetukset" }, "websiteUri": { - "message": "Website (URI)" + "message": "Verkkosivusto (URI)" }, "websiteUriCount": { - "message": "Website (URI) $COUNT$", + "message": "Verkkosivusto (URI) $COUNT$", "description": "Label for an input field that contains a website URI. The input field is part of a list of fields, and the count indicates the position of the field in the list.", "placeholders": { "count": { @@ -413,43 +413,43 @@ } }, "websiteAdded": { - "message": "Website added" + "message": "Verkkosivusto lisättiin" }, "addWebsite": { - "message": "Add website" + "message": "Lisää verkkosivusto" }, "deleteWebsite": { - "message": "Delete website" + "message": "Poista verkkosivusto" }, "owner": { - "message": "Owner" + "message": "Omistaja" }, "addField": { - "message": "Add field" + "message": "Lisää kenttä" }, "fieldType": { - "message": "Field type" + "message": "Kentän tyyppi" }, "fieldLabel": { - "message": "Field label" + "message": "Kentän nimi" }, "add": { - "message": "Add" + "message": "Lisää" }, "textHelpText": { - "message": "Use text fields for data like security questions" + "message": "Käytä tekstikenttiä esimerkiksi turvakysymysten kaltaisille tiedoille" }, "hiddenHelpText": { - "message": "Use hidden fields for sensitive data like a password" + "message": "Käytä piilotettuja kenttiä esimerkiksi salasanojen kaltaisille arkaluonteisille tiedoille" }, "checkBoxHelpText": { - "message": "Use checkboxes if you'd like to autofill a form's checkbox, like a remember email" + "message": "Käytä valintaruutuja esimerkiksi sähköpostiosoitteen muistamisen kaltaisten valintaruutujen automaattiseen merkintään" }, "linkedHelpText": { - "message": "Use a linked field when you are experiencing autofill issues for a specific website." + "message": "Käytä linkitettyjä kenttiä kohdatessasi sivustokohtaisia automaattitäytön ongelmia." }, "linkedLabelHelpText": { - "message": "Enter the the field's html id, name, aria-label, or placeholder." + "message": "Syötä kentän HTML-koodista löytyvä id-, name-, aria-label- tai placeholder-arvo." }, "folder": { "message": "Kansio" @@ -477,7 +477,7 @@ "description": "This describes a field that is 'linked' (related) to another field." }, "cfTypeCheckbox": { - "message": "Checkbox" + "message": "Valintaruutu" }, "linkedValue": { "message": "Linkitetty arvo", @@ -1977,10 +1977,10 @@ } }, "cardDetails": { - "message": "Card details" + "message": "Kortin tiedot" }, "cardBrandDetails": { - "message": "$BRAND$ details", + "message": "$BRAND$-tiedot", "placeholders": { "brand": { "content": "$1", @@ -1989,29 +1989,29 @@ } }, "learnMoreAboutAuthenticators": { - "message": "Learn more about authenticators" + "message": "Lue lisää todentajista" }, "copyTOTP": { - "message": "Copy Authenticator key (TOTP)" + "message": "Kopioi todennusavain (TOTP)" }, "totpHelperTitle": { - "message": "Make 2-step verification seamless" + "message": "Tee kaksivaiheisesta kirjautumisesta saumatonta" }, "totpHelper": { - "message": "Bitwarden can store and fill 2-step verification codes. Copy and paste the key into this field." + "message": "Bitwarden voi säilyttää ja täyttää kaksivaiheisen kirjautumisen koodit. Kopioi ja liitä todennusavain tähän kenttään." }, "totpHelperWithCapture": { - "message": "Bitwarden can store and fill 2-step verification codes. Select the camera icon to take a screenshot of this website's authenticator QR code, or copy and paste the key into this field." + "message": "Bitwarden voi säilyttää ja täyttää kaksivaiheisen kirjautumisen koodit. Kamerakuvakkeella voit kaapata todennusavaimen avoimen sivun QR-koodista automaattisesti, tai voit kopioida ja liittää sen tähän kenttään manuaalisesti." }, "premium": { "message": "Premium", "description": "Premium membership" }, "cardExpiredTitle": { - "message": "Expired card" + "message": "Vanhentunut kortti" }, "cardExpiredMessage": { - "message": "If you've renewed it, update the card's information" + "message": "Jos olet uudistanut kortin, päivitä sen tiedot" }, "verificationRequired": { "message": "Vahvistus vaaditaan", @@ -2183,13 +2183,13 @@ "message": "Organisaatiokäytäntö estää kohteiden tuonnin yksityiseen holviisi." }, "personalDetails": { - "message": "Personal details" + "message": "Henkilökohtaiset tiedot" }, "identification": { "message": "Identification" }, "contactInfo": { - "message": "Contact information" + "message": "Yhteystiedot" }, "allSends": { "message": "Kaikki Sendit", @@ -2626,7 +2626,7 @@ "message": "Luo sähköpostiosoite" }, "usernameGenerator": { - "message": "Username generator" + "message": "Käyttäjätunnusgeneraattori" }, "spinboxBoundariesHint": { "message": "Arvon tulee olla väliltä $MIN$—$MAX$.", @@ -3550,10 +3550,10 @@ "message": "Kertakirjautumiselle ei löytynyt vapaita portteja." }, "securePasswordGenerated": { - "message": "Secure password generated! Don't forget to also update your password on the website." + "message": "Turvallinen salasana luotiin! Muista vaihtaa se myös verkkosivuston tiliasetuksiin." }, "useGeneratorHelpTextPartOne": { - "message": "Use the generator", + "message": "Käytä generaattoria", "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" }, "useGeneratorHelpTextPartTwo": { @@ -3567,10 +3567,10 @@ "message": "Biometrinen lukituksen avaus ei ole tällä hetkellä käytettävissä." }, "biometricsStatusHelptextAutoSetupNeeded": { - "message": "Biometric unlock is unavailable due to misconfigured system files." + "message": "Biometrinen avaus ei ole käytettävissä virheellisesti määritettyjen järjestelmätiedostojen takia." }, "biometricsStatusHelptextManualSetupNeeded": { - "message": "Biometric unlock is unavailable due to misconfigured system files." + "message": "Biometrinen avaus ei ole käytettävissä virheellisesti määritettyjen järjestelmätiedostojen takia." }, "biometricsStatusHelptextNotEnabledLocally": { "message": "Biometrinen avaus ei ole käytettävissä, koska sitä ei ole otettu käyttöön osoitteelle $EMAIL$ Bitwardenin työpöytäsovelluksessa.", @@ -3597,7 +3597,7 @@ "message": "Warning: Agent Forwarding" }, "agentForwardingWarningText": { - "message": "This request comes from a remote device that you are logged into" + "message": "Tämä pyyntö tulee etälaitteelta, johon olet kirjautunut" }, "sshkeyApprovalMessageInfix": { "message": "pyytää pääsyä" @@ -3639,25 +3639,25 @@ "message": "Määritä kaksivaiheinen kirjautuminen" }, "itemDetails": { - "message": "Item details" + "message": "Kohteen tiedot" }, "itemName": { - "message": "Item name" + "message": "Kohteen nimi" }, "loginCredentials": { - "message": "Login credentials" + "message": "Kirjautumistiedot" }, "additionalOptions": { - "message": "Additional options" + "message": "Lisävalinnat" }, "itemHistory": { - "message": "Item history" + "message": "Kohteen historia" }, "lastEdited": { "message": "Last edited" }, "upload": { - "message": "Upload" + "message": "Lähetä" }, "newDeviceVerificationNoticeContentPage1": { "message": "Bitwarden lähettää tilisi sähköpostiosoitteeseen koodin, jolla voit vahvistaa kirjautumiset uusista laitteista helmikuusta 2025 alkaen." @@ -3712,5 +3712,35 @@ }, "move": { "message": "Siirrä" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } } diff --git a/apps/desktop/src/locales/fil/messages.json b/apps/desktop/src/locales/fil/messages.json index cdad0fe5487..2735aef242e 100644 --- a/apps/desktop/src/locales/fil/messages.json +++ b/apps/desktop/src/locales/fil/messages.json @@ -3712,5 +3712,35 @@ }, "move": { "message": "Move" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } } diff --git a/apps/desktop/src/locales/fr/messages.json b/apps/desktop/src/locales/fr/messages.json index bb019d64f36..0170cf2d0ce 100644 --- a/apps/desktop/src/locales/fr/messages.json +++ b/apps/desktop/src/locales/fr/messages.json @@ -3712,5 +3712,35 @@ }, "move": { "message": "Move" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } } diff --git a/apps/desktop/src/locales/gl/messages.json b/apps/desktop/src/locales/gl/messages.json index 81e3a94ff4d..2350e0df4c7 100644 --- a/apps/desktop/src/locales/gl/messages.json +++ b/apps/desktop/src/locales/gl/messages.json @@ -3712,5 +3712,35 @@ }, "move": { "message": "Move" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } } diff --git a/apps/desktop/src/locales/he/messages.json b/apps/desktop/src/locales/he/messages.json index 7c3d30d7bbe..a8f52de239f 100644 --- a/apps/desktop/src/locales/he/messages.json +++ b/apps/desktop/src/locales/he/messages.json @@ -3712,5 +3712,35 @@ }, "move": { "message": "Move" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } } diff --git a/apps/desktop/src/locales/hi/messages.json b/apps/desktop/src/locales/hi/messages.json index add8b81f429..a5d273d0e25 100644 --- a/apps/desktop/src/locales/hi/messages.json +++ b/apps/desktop/src/locales/hi/messages.json @@ -3712,5 +3712,35 @@ }, "move": { "message": "Move" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } } diff --git a/apps/desktop/src/locales/hr/messages.json b/apps/desktop/src/locales/hr/messages.json index 0a9b7be1fd9..af78a035927 100644 --- a/apps/desktop/src/locales/hr/messages.json +++ b/apps/desktop/src/locales/hr/messages.json @@ -3712,5 +3712,35 @@ }, "move": { "message": "Move" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } } diff --git a/apps/desktop/src/locales/hu/messages.json b/apps/desktop/src/locales/hu/messages.json index 6b145ce80db..ae97ccd49e6 100644 --- a/apps/desktop/src/locales/hu/messages.json +++ b/apps/desktop/src/locales/hu/messages.json @@ -3712,5 +3712,35 @@ }, "move": { "message": "Áthelyezés" + }, + "newLoginNudgeTitle": { + "message": "Idő megtakarítás automatikus kitöltéssel" + }, + "newLoginNudgeBody": { + "message": "Adjunk meg egy webhelyet, hogy ez a bejelentkezési név automatikusan kitöltendő javaslatként jelenjen meg." + }, + "newCardNudgeTitle": { + "message": "Zökkenőmentes online fizetés" + }, + "newCardNudgeBody": { + "message": "Kártyákkal könnyedén, biztonságosan és pontosan tölthetjük ki automatikusan a fizetési űrlapokat." + }, + "newIdentityNudgeTitle": { + "message": "Egyszerűsítsük a fiókok létrehozását" + }, + "newIdentityNudgeBody": { + "message": "Azonosítókkal gyorsan automatikusan kitölthetjük a hosszú regisztrációs vagy kapcsolatfelvételi űrlapokat." + }, + "newNoteNudgeTitle": { + "message": "Tartsuk biztonságban az érzékeny adatokat" + }, + "newNoteNudgeBody": { + "message": "Jegyzetekkel biztonságosan tárolhatjuk az érzékeny adatokat, például a banki vagy biztosítási adatokat." + }, + "newSshNudgeTitle": { + "message": "Fejlesztőbarát SSH hozzáférés" + }, + "newSshNudgeBody": { + "message": "Tároljuk a kulcsokat és csatlakozzunk az SSH-ügynökhöz a gyors, titkosított hitelesítéshez." } } diff --git a/apps/desktop/src/locales/id/messages.json b/apps/desktop/src/locales/id/messages.json index 7afef92ee3e..e54bcece504 100644 --- a/apps/desktop/src/locales/id/messages.json +++ b/apps/desktop/src/locales/id/messages.json @@ -3712,5 +3712,35 @@ }, "move": { "message": "Move" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } } diff --git a/apps/desktop/src/locales/it/messages.json b/apps/desktop/src/locales/it/messages.json index e1aca77337e..f02823d09de 100644 --- a/apps/desktop/src/locales/it/messages.json +++ b/apps/desktop/src/locales/it/messages.json @@ -3712,5 +3712,35 @@ }, "move": { "message": "Move" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } } diff --git a/apps/desktop/src/locales/ja/messages.json b/apps/desktop/src/locales/ja/messages.json index 0bddf31cdb3..dc87276cfe3 100644 --- a/apps/desktop/src/locales/ja/messages.json +++ b/apps/desktop/src/locales/ja/messages.json @@ -3712,5 +3712,35 @@ }, "move": { "message": "移動" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } } diff --git a/apps/desktop/src/locales/ka/messages.json b/apps/desktop/src/locales/ka/messages.json index ff3c146ca52..dd01fa84df2 100644 --- a/apps/desktop/src/locales/ka/messages.json +++ b/apps/desktop/src/locales/ka/messages.json @@ -3712,5 +3712,35 @@ }, "move": { "message": "Move" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } } diff --git a/apps/desktop/src/locales/km/messages.json b/apps/desktop/src/locales/km/messages.json index 81e3a94ff4d..2350e0df4c7 100644 --- a/apps/desktop/src/locales/km/messages.json +++ b/apps/desktop/src/locales/km/messages.json @@ -3712,5 +3712,35 @@ }, "move": { "message": "Move" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } } diff --git a/apps/desktop/src/locales/kn/messages.json b/apps/desktop/src/locales/kn/messages.json index a599cef168a..b0f9ac4849f 100644 --- a/apps/desktop/src/locales/kn/messages.json +++ b/apps/desktop/src/locales/kn/messages.json @@ -3712,5 +3712,35 @@ }, "move": { "message": "Move" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } } diff --git a/apps/desktop/src/locales/ko/messages.json b/apps/desktop/src/locales/ko/messages.json index ff6f9b4b8f1..ed94aae47e8 100644 --- a/apps/desktop/src/locales/ko/messages.json +++ b/apps/desktop/src/locales/ko/messages.json @@ -3712,5 +3712,35 @@ }, "move": { "message": "Move" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } } diff --git a/apps/desktop/src/locales/lt/messages.json b/apps/desktop/src/locales/lt/messages.json index ab8a991ca72..00a5602ff3c 100644 --- a/apps/desktop/src/locales/lt/messages.json +++ b/apps/desktop/src/locales/lt/messages.json @@ -3712,5 +3712,35 @@ }, "move": { "message": "Move" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } } diff --git a/apps/desktop/src/locales/lv/messages.json b/apps/desktop/src/locales/lv/messages.json index f6d8cbed4d6..4a3ed41f980 100644 --- a/apps/desktop/src/locales/lv/messages.json +++ b/apps/desktop/src/locales/lv/messages.json @@ -980,7 +980,7 @@ "message": "Atlasīt divpakāpju pieteikšanās veidu" }, "selfHostedEnvironment": { - "message": "Pašuzturēta vide" + "message": "Pašmitināta vide" }, "selfHostedBaseUrlHint": { "message": "Jānorāda sava pašizvietotā Bitward servera pamata URL. Piemērs: https://bitwarden.uznemums.lv" @@ -1145,10 +1145,10 @@ "message": "Iegūt pārlūka paplašinājumu" }, "syncingComplete": { - "message": "Sinhronizācija pabeigta" + "message": "Sinhronizēšana pabeigta" }, "syncingFailed": { - "message": "Sinhronizācija neizdevās" + "message": "Sinhronizēšana neizdevās" }, "yourVaultIsLocked": { "message": "Glabātava ir aizslēgta. Jāapliecina sava identitāte, lai turpinātu." @@ -2508,7 +2508,7 @@ "message": "Galvenā parole tika noņemta." }, "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ izmanto vienoto pieteikšanos ar pašizvietotu atslēgu serveri. Tās dalībniekiem vairs nav nepieciešama galvenā parole, lai pieteiktos.", + "message": "$ORGANIZATION$ izmanto vienoto pieteikšanos ar pašmitinātu atslēgu serveri. Tās dalībniekiem vairs nav nepieciešama galvenā parole, lai pieteiktos.", "placeholders": { "organization": { "content": "$1", @@ -3123,7 +3123,7 @@ "description": "European Union" }, "selfHostedServer": { - "message": "pašizvietots" + "message": "pašmitināts" }, "accessDenied": { "message": "Piekļuve liegta. Nav nepieciešamo atļauju, lai skatītu šo lapu." @@ -3712,5 +3712,35 @@ }, "move": { "message": "Pārvietot" + }, + "newLoginNudgeTitle": { + "message": "Laika ietaupīšana ar automātisko aizpildi" + }, + "newLoginNudgeBody": { + "message": "Iekļauj tīmekļvietni, lai šis pieteikšanās vienums parādītos kā automātiskās aizpildes ieteikums!" + }, + "newCardNudgeTitle": { + "message": "Plūdena apmaksa tiešsaistē" + }, + "newCardNudgeBody": { + "message": "Ar kartēm ir viegli automātiski aizpildīt maksājumu veidlapu droši un rūpīgi." + }, + "newIdentityNudgeTitle": { + "message": "Kontu izveidošanas vienkāršošana" + }, + "newIdentityNudgeBody": { + "message": "Ar identitātēm var ātri automātiski aizpildīt garas reģistrēšanās vai saziņas veidlapas." + }, + "newNoteNudgeTitle": { + "message": "Turi savus jūtīgos datus drošībā" + }, + "newNoteNudgeBody": { + "message": "Piezīmēs var droši glabāt jūtīgus datus, piemēram, bankas vai apdrošināšanas informāciju." + }, + "newSshNudgeTitle": { + "message": "Izstrādātājiem draudzīga SSH piekļuve" + }, + "newSshNudgeBody": { + "message": "Glabā savas atslēgas un savienojies ar SSH aģentu ātrai, šifrētai autentificēšanai!" } } diff --git a/apps/desktop/src/locales/me/messages.json b/apps/desktop/src/locales/me/messages.json index abb4f972898..2ee83d6e91b 100644 --- a/apps/desktop/src/locales/me/messages.json +++ b/apps/desktop/src/locales/me/messages.json @@ -3712,5 +3712,35 @@ }, "move": { "message": "Move" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } } diff --git a/apps/desktop/src/locales/ml/messages.json b/apps/desktop/src/locales/ml/messages.json index 92ee37940fc..9ce30119832 100644 --- a/apps/desktop/src/locales/ml/messages.json +++ b/apps/desktop/src/locales/ml/messages.json @@ -3712,5 +3712,35 @@ }, "move": { "message": "Move" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } } diff --git a/apps/desktop/src/locales/mr/messages.json b/apps/desktop/src/locales/mr/messages.json index 81e3a94ff4d..2350e0df4c7 100644 --- a/apps/desktop/src/locales/mr/messages.json +++ b/apps/desktop/src/locales/mr/messages.json @@ -3712,5 +3712,35 @@ }, "move": { "message": "Move" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } } diff --git a/apps/desktop/src/locales/my/messages.json b/apps/desktop/src/locales/my/messages.json index 06cbb78f922..b53b3fb241a 100644 --- a/apps/desktop/src/locales/my/messages.json +++ b/apps/desktop/src/locales/my/messages.json @@ -3712,5 +3712,35 @@ }, "move": { "message": "Move" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } } diff --git a/apps/desktop/src/locales/nb/messages.json b/apps/desktop/src/locales/nb/messages.json index e1e9114db49..f0e6b338bc8 100644 --- a/apps/desktop/src/locales/nb/messages.json +++ b/apps/desktop/src/locales/nb/messages.json @@ -3712,5 +3712,35 @@ }, "move": { "message": "Move" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } } diff --git a/apps/desktop/src/locales/ne/messages.json b/apps/desktop/src/locales/ne/messages.json index 2f68046ec3a..93485f6d773 100644 --- a/apps/desktop/src/locales/ne/messages.json +++ b/apps/desktop/src/locales/ne/messages.json @@ -3712,5 +3712,35 @@ }, "move": { "message": "Move" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } } diff --git a/apps/desktop/src/locales/nl/messages.json b/apps/desktop/src/locales/nl/messages.json index f149dbf158f..dee51830601 100644 --- a/apps/desktop/src/locales/nl/messages.json +++ b/apps/desktop/src/locales/nl/messages.json @@ -3712,5 +3712,35 @@ }, "move": { "message": "Verplaatsen" + }, + "newLoginNudgeTitle": { + "message": "Tijd besparen met automatisch aanvullen" + }, + "newLoginNudgeBody": { + "message": "Voeg een website toe zodat deze login wordt weergegeven als een automatische invulsuggestie." + }, + "newCardNudgeTitle": { + "message": "Naadloos online afrekenen" + }, + "newCardNudgeBody": { + "message": "Met kaarten gemakkelijk, veilig en accuraat automatisch invullen van betaalformulieren." + }, + "newIdentityNudgeTitle": { + "message": "Vereenvoudig het aanmaken van accounts" + }, + "newIdentityNudgeBody": { + "message": "Met identiteiten vul je lange registratie- of contactformulieren snel automatisch in." + }, + "newNoteNudgeTitle": { + "message": "Houd je gevoelige gegevens veilig" + }, + "newNoteNudgeBody": { + "message": "Met notities veilig opslaan van gevoelige gegevens zoals bank- of verzekeringsgegevens." + }, + "newSshNudgeTitle": { + "message": "Ontwikkelaars-vriendelijke SSH-toegang" + }, + "newSshNudgeBody": { + "message": "Sla je sleutels op en verbind met de SSH-agent voor snelle, versleutelde authenticatie." } } diff --git a/apps/desktop/src/locales/nn/messages.json b/apps/desktop/src/locales/nn/messages.json index ffa70f3e737..894b505221b 100644 --- a/apps/desktop/src/locales/nn/messages.json +++ b/apps/desktop/src/locales/nn/messages.json @@ -3712,5 +3712,35 @@ }, "move": { "message": "Move" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } } diff --git a/apps/desktop/src/locales/or/messages.json b/apps/desktop/src/locales/or/messages.json index c4c923b662b..0e99fbcda3e 100644 --- a/apps/desktop/src/locales/or/messages.json +++ b/apps/desktop/src/locales/or/messages.json @@ -3712,5 +3712,35 @@ }, "move": { "message": "Move" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } } diff --git a/apps/desktop/src/locales/pl/messages.json b/apps/desktop/src/locales/pl/messages.json index e5a8cdf77d4..c6000b84e7a 100644 --- a/apps/desktop/src/locales/pl/messages.json +++ b/apps/desktop/src/locales/pl/messages.json @@ -3712,5 +3712,35 @@ }, "move": { "message": "Move" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } } diff --git a/apps/desktop/src/locales/pt_BR/messages.json b/apps/desktop/src/locales/pt_BR/messages.json index be20f43a0e3..35d63e07b1a 100644 --- a/apps/desktop/src/locales/pt_BR/messages.json +++ b/apps/desktop/src/locales/pt_BR/messages.json @@ -3712,5 +3712,35 @@ }, "move": { "message": "Mover" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } } diff --git a/apps/desktop/src/locales/pt_PT/messages.json b/apps/desktop/src/locales/pt_PT/messages.json index e721133c325..50163e811ba 100644 --- a/apps/desktop/src/locales/pt_PT/messages.json +++ b/apps/desktop/src/locales/pt_PT/messages.json @@ -3712,5 +3712,35 @@ }, "move": { "message": "Mover" + }, + "newLoginNudgeTitle": { + "message": "Poupe tempo com o preenchimento automático" + }, + "newLoginNudgeBody": { + "message": "Inclua um site para que esta credencial apareça como uma sugestão de preenchimento automático." + }, + "newCardNudgeTitle": { + "message": "Pagamentos online sem problemas" + }, + "newCardNudgeBody": { + "message": "Com os cartões, preencha facilmente formulários de pagamento de forma segura e exata." + }, + "newIdentityNudgeTitle": { + "message": "Simplifique a criação de contas" + }, + "newIdentityNudgeBody": { + "message": "Com as identidades, preencha rapidamente formulários de registo ou de contacto longos." + }, + "newNoteNudgeTitle": { + "message": "Mantenha os seus dados sensíveis seguros" + }, + "newNoteNudgeBody": { + "message": "Com as notas, armazene de forma segura dados sensíveis, como dados bancários ou de seguros." + }, + "newSshNudgeTitle": { + "message": "Acesso SSH de fácil utilização pelos programadores" + }, + "newSshNudgeBody": { + "message": "Guarde as suas chaves e ligue-se ao agente SSH para uma autenticação rápida e encriptada." } } diff --git a/apps/desktop/src/locales/ro/messages.json b/apps/desktop/src/locales/ro/messages.json index 1e37615ff21..80b87b73f50 100644 --- a/apps/desktop/src/locales/ro/messages.json +++ b/apps/desktop/src/locales/ro/messages.json @@ -3712,5 +3712,35 @@ }, "move": { "message": "Move" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } } diff --git a/apps/desktop/src/locales/ru/messages.json b/apps/desktop/src/locales/ru/messages.json index 76a11b7868e..b1f3c35e9be 100644 --- a/apps/desktop/src/locales/ru/messages.json +++ b/apps/desktop/src/locales/ru/messages.json @@ -3712,5 +3712,35 @@ }, "move": { "message": "Переместить" + }, + "newLoginNudgeTitle": { + "message": "Экономьте время с помощью автозаполнения" + }, + "newLoginNudgeBody": { + "message": "Включите сайт, чтобы этот логин отображался в качестве предложения для автозаполнения." + }, + "newCardNudgeTitle": { + "message": "Оформление заказа через интернет" + }, + "newCardNudgeBody": { + "message": "С помощью карт можно легко и безопасно автоматически заполнять формы оплаты." + }, + "newIdentityNudgeTitle": { + "message": "Упрощение создания аккаунтов" + }, + "newIdentityNudgeBody": { + "message": "С помощью личностей можно быстро заполнять длинные регистрационные или контактные формы." + }, + "newNoteNudgeTitle": { + "message": "Храните ваши конфиденциальные данные в безопасности" + }, + "newNoteNudgeBody": { + "message": "С помощью заметок можно надежно хранить конфиденциальные данные, например, банковские или страховые реквизиты." + }, + "newSshNudgeTitle": { + "message": "Удобный для разработчиков SSH-доступ" + }, + "newSshNudgeBody": { + "message": "Храните свои ключи и подключайтесь с помощью агента SSH для быстрой и зашифрованной аутентификации." } } diff --git a/apps/desktop/src/locales/si/messages.json b/apps/desktop/src/locales/si/messages.json index a697124f72c..d345f7849a0 100644 --- a/apps/desktop/src/locales/si/messages.json +++ b/apps/desktop/src/locales/si/messages.json @@ -3712,5 +3712,35 @@ }, "move": { "message": "Move" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } } diff --git a/apps/desktop/src/locales/sk/messages.json b/apps/desktop/src/locales/sk/messages.json index d8bc74f1b0b..a65d6c24f5a 100644 --- a/apps/desktop/src/locales/sk/messages.json +++ b/apps/desktop/src/locales/sk/messages.json @@ -3712,5 +3712,35 @@ }, "move": { "message": "Presunúť" + }, + "newLoginNudgeTitle": { + "message": "Ušetrite čas s automatickým vypĺňaním" + }, + "newLoginNudgeBody": { + "message": "Zadajte webovú stránku, aby sa tieto prihlasovacie údaje zobrazili ako návrh na automatické vyplnenie." + }, + "newCardNudgeTitle": { + "message": "Bezproblémová online registrácia" + }, + "newCardNudgeBody": { + "message": "S kartami môžete jednoducho, bezpečne a presne automaticky vypĺňať platobné formuláre." + }, + "newIdentityNudgeTitle": { + "message": "Zjednodušenie vytvárania účtov" + }, + "newIdentityNudgeBody": { + "message": "Pomocou identít môžete rýchlo automaticky vypĺňať dlhé registračné alebo kontaktné formuláre." + }, + "newNoteNudgeTitle": { + "message": "Udržujte svoje citlivé údaje v bezpečí" + }, + "newNoteNudgeBody": { + "message": "Pomocou poznámok môžete bezpečne ukladať citlivé údaje, napríklad bankové údaje alebo údaje o poistení." + }, + "newSshNudgeTitle": { + "message": "Prístup SSH vhodný pre vývojárov" + }, + "newSshNudgeBody": { + "message": "Uložte si kľúče a pripojte sa pomocou agenta SSH na rýchle šifrované overovanie." } } diff --git a/apps/desktop/src/locales/sl/messages.json b/apps/desktop/src/locales/sl/messages.json index ecfc8b9c00b..fd4c17f949e 100644 --- a/apps/desktop/src/locales/sl/messages.json +++ b/apps/desktop/src/locales/sl/messages.json @@ -3712,5 +3712,35 @@ }, "move": { "message": "Move" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } } diff --git a/apps/desktop/src/locales/sr/messages.json b/apps/desktop/src/locales/sr/messages.json index 195b816a2ba..aa0ac47cd9d 100644 --- a/apps/desktop/src/locales/sr/messages.json +++ b/apps/desktop/src/locales/sr/messages.json @@ -394,16 +394,16 @@ "message": "Једнократни код" }, "authenticatorKey": { - "message": "Authenticator key" + "message": "Кључ аутентификатора" }, "autofillOptions": { - "message": "Autofill options" + "message": "Опције Ауто-пуњења" }, "websiteUri": { - "message": "Website (URI)" + "message": "Вебсајт (URI)" }, "websiteUriCount": { - "message": "Website (URI) $COUNT$", + "message": "Сајт (URI) $COUNT$", "description": "Label for an input field that contains a website URI. The input field is part of a list of fields, and the count indicates the position of the field in the list.", "placeholders": { "count": { @@ -413,43 +413,43 @@ } }, "websiteAdded": { - "message": "Website added" + "message": "Вебсајт додат" }, "addWebsite": { - "message": "Add website" + "message": "Додај вебсајт" }, "deleteWebsite": { - "message": "Delete website" + "message": "Обриши вебсајт" }, "owner": { - "message": "Owner" + "message": "Власник" }, "addField": { - "message": "Add field" + "message": "Додај поље" }, "fieldType": { - "message": "Field type" + "message": "Врста поља" }, "fieldLabel": { - "message": "Field label" + "message": "Ознака поља" }, "add": { - "message": "Add" + "message": "Додај" }, "textHelpText": { - "message": "Use text fields for data like security questions" + "message": "Користите текстуална поља за податке као што су безбедносна питања" }, "hiddenHelpText": { - "message": "Use hidden fields for sensitive data like a password" + "message": "Користите скривена поља за осетљиве податке као што је лозинка" }, "checkBoxHelpText": { - "message": "Use checkboxes if you'd like to autofill a form's checkbox, like a remember email" + "message": "Користите поља за потврду ако желите да аутоматски попуните поље за потврду обрасца, на пример имејл за памћење" }, "linkedHelpText": { - "message": "Use a linked field when you are experiencing autofill issues for a specific website." + "message": "Користите повезано поље када имате проблема са аутоматским попуњавањем за одређену веб локацију." }, "linkedLabelHelpText": { - "message": "Enter the the field's html id, name, aria-label, or placeholder." + "message": "Унесите html Ид поља, име, aria-label, или placeholder." }, "folder": { "message": "Фасцикла" @@ -477,7 +477,7 @@ "description": "This describes a field that is 'linked' (related) to another field." }, "cfTypeCheckbox": { - "message": "Checkbox" + "message": "Поље за потврду" }, "linkedValue": { "message": "Вредност повезана", @@ -1977,10 +1977,10 @@ } }, "cardDetails": { - "message": "Card details" + "message": "Детаљи картице" }, "cardBrandDetails": { - "message": "$BRAND$ details", + "message": "$BRAND$ детаљи", "placeholders": { "brand": { "content": "$1", @@ -1989,29 +1989,29 @@ } }, "learnMoreAboutAuthenticators": { - "message": "Learn more about authenticators" + "message": "Сазнајте више о аутентификаторима" }, "copyTOTP": { - "message": "Copy Authenticator key (TOTP)" + "message": "Копирати једнократни кôд (TOTP)" }, "totpHelperTitle": { - "message": "Make 2-step verification seamless" + "message": "Учините верификацију у 2 корака беспрекорном" }, "totpHelper": { - "message": "Bitwarden can store and fill 2-step verification codes. Copy and paste the key into this field." + "message": "Bitwarden може да чува и попуњава верификационе кодове у 2 корака. Копирајте и налепите кључ у ово поље." }, "totpHelperWithCapture": { - "message": "Bitwarden can store and fill 2-step verification codes. Select the camera icon to take a screenshot of this website's authenticator QR code, or copy and paste the key into this field." + "message": "Bitwarden може да чува и попуњава верификационе кодове у 2 корака. Изаберите икону камере да бисте направили снимак екрана QR кода за аутентификацију ове веб локације или копирајте и налепите кључ у ово поље." }, "premium": { - "message": "Premium", + "message": "Премијум", "description": "Premium membership" }, "cardExpiredTitle": { - "message": "Expired card" + "message": "Картица је истекла" }, "cardExpiredMessage": { - "message": "If you've renewed it, update the card's information" + "message": "Ако сте је обновили, ажурирајте податке о картици" }, "verificationRequired": { "message": "Потребдна верификација", @@ -2183,13 +2183,13 @@ "message": "Политика организације је блокирала увоз ставки у ваш појединачни сеф." }, "personalDetails": { - "message": "Personal details" + "message": "Личне информације" }, "identification": { - "message": "Identification" + "message": "Идентификација" }, "contactInfo": { - "message": "Contact information" + "message": "Контакт подаци" }, "allSends": { "message": "Сва слања", @@ -2626,7 +2626,7 @@ "message": "Генеришите имејл" }, "usernameGenerator": { - "message": "Username generator" + "message": "Генератор корисничког имена" }, "spinboxBoundariesHint": { "message": "Вредност мора бити између $MIN$ и $MAX$.", @@ -3323,7 +3323,7 @@ "message": "Следите наведене кораке да бисте завршили пријављивање." }, "followTheStepsBelowToFinishLoggingInWithSecurityKey": { - "message": "Follow the steps below to finish logging in with your security key." + "message": "Следите наведене кораке да бисте завршили пријаву са својим безбедносним кључем." }, "launchDuo": { "message": "Покренути Duo у претраживачу" @@ -3550,14 +3550,14 @@ "message": "Нису пронађени портови за SSO пријаву." }, "securePasswordGenerated": { - "message": "Secure password generated! Don't forget to also update your password on the website." + "message": "Сигурна лозинка је генерисана! Не заборавите да ажурирате и своју лозинку на веб локацији." }, "useGeneratorHelpTextPartOne": { - "message": "Use the generator", + "message": "Употребити генератор", "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" }, "useGeneratorHelpTextPartTwo": { - "message": "to create a strong unique password", + "message": "да креирате јаку јединствену лозинку", "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" }, "biometricsStatusHelptextUnlockNeeded": { @@ -3639,25 +3639,25 @@ "message": "Поставити дво-степенску пријаву" }, "itemDetails": { - "message": "Item details" + "message": "Детаљи ставке" }, "itemName": { - "message": "Item name" + "message": "Име ставке" }, "loginCredentials": { - "message": "Login credentials" + "message": "Акредитиве за пријављивање" }, "additionalOptions": { - "message": "Additional options" + "message": "Додатне опције" }, "itemHistory": { - "message": "Item history" + "message": "Историја предмета" }, "lastEdited": { - "message": "Last edited" + "message": "Последња измена" }, "upload": { - "message": "Upload" + "message": "Отпреми" }, "newDeviceVerificationNoticeContentPage1": { "message": "Bitwarden ће послати кôд на имејл вашег налога за верификовање пријављивања са нових уређаја почевши од фебруара 2025." @@ -3712,5 +3712,35 @@ }, "move": { "message": "Премести" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } } diff --git a/apps/desktop/src/locales/sv/messages.json b/apps/desktop/src/locales/sv/messages.json index 84828a05130..5ca6c5b9372 100644 --- a/apps/desktop/src/locales/sv/messages.json +++ b/apps/desktop/src/locales/sv/messages.json @@ -3712,5 +3712,35 @@ }, "move": { "message": "Move" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } } diff --git a/apps/desktop/src/locales/te/messages.json b/apps/desktop/src/locales/te/messages.json index 81e3a94ff4d..2350e0df4c7 100644 --- a/apps/desktop/src/locales/te/messages.json +++ b/apps/desktop/src/locales/te/messages.json @@ -3712,5 +3712,35 @@ }, "move": { "message": "Move" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } } diff --git a/apps/desktop/src/locales/th/messages.json b/apps/desktop/src/locales/th/messages.json index c027755373f..6bd75b86398 100644 --- a/apps/desktop/src/locales/th/messages.json +++ b/apps/desktop/src/locales/th/messages.json @@ -3712,5 +3712,35 @@ }, "move": { "message": "Move" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } } diff --git a/apps/desktop/src/locales/tr/messages.json b/apps/desktop/src/locales/tr/messages.json index 9f62e072700..3100cbabcbb 100644 --- a/apps/desktop/src/locales/tr/messages.json +++ b/apps/desktop/src/locales/tr/messages.json @@ -3712,5 +3712,35 @@ }, "move": { "message": "Taşı" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } } diff --git a/apps/desktop/src/locales/uk/messages.json b/apps/desktop/src/locales/uk/messages.json index 099039fd5bd..9b751b4830a 100644 --- a/apps/desktop/src/locales/uk/messages.json +++ b/apps/desktop/src/locales/uk/messages.json @@ -3712,5 +3712,35 @@ }, "move": { "message": "Перемістити" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } } diff --git a/apps/desktop/src/locales/vi/messages.json b/apps/desktop/src/locales/vi/messages.json index b555eb49bae..d75175778a3 100644 --- a/apps/desktop/src/locales/vi/messages.json +++ b/apps/desktop/src/locales/vi/messages.json @@ -3712,5 +3712,35 @@ }, "move": { "message": "Move" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } } diff --git a/apps/desktop/src/locales/zh_CN/messages.json b/apps/desktop/src/locales/zh_CN/messages.json index cba4968d45d..7085d4aa033 100644 --- a/apps/desktop/src/locales/zh_CN/messages.json +++ b/apps/desktop/src/locales/zh_CN/messages.json @@ -2189,7 +2189,7 @@ "message": "Identification" }, "contactInfo": { - "message": "Contact information" + "message": "联系信息" }, "allSends": { "message": "所有的 Send", @@ -3550,7 +3550,7 @@ "message": "未找到用于 SSO 登录的空闲端口。" }, "securePasswordGenerated": { - "message": "安全的密码生成好了!别忘了在网站上也更新一下您的密码。" + "message": "安全的密码已生成!不要忘记在网站上更新您的密码。" }, "useGeneratorHelpTextPartOne": { "message": "Use the generator", @@ -3712,5 +3712,35 @@ }, "move": { "message": "移动" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } } diff --git a/apps/desktop/src/locales/zh_TW/messages.json b/apps/desktop/src/locales/zh_TW/messages.json index 40830f1d2e0..a871a07adda 100644 --- a/apps/desktop/src/locales/zh_TW/messages.json +++ b/apps/desktop/src/locales/zh_TW/messages.json @@ -3712,5 +3712,35 @@ }, "move": { "message": "Move" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } } From da15a4ce8de5a6115ca2a250868b88349523cd82 Mon Sep 17 00:00:00 2001 From: Conner Turnbull <133619638+cturnbull-bitwarden@users.noreply.github.com> Date: Fri, 2 May 2025 08:47:55 -0400 Subject: [PATCH 118/499] [PM-20403] Updated F4E copy (#14404) * Updated F4E copy * Updated incorrect i18n key --- .../app/billing/members/free-bitwarden-families.component.html | 2 +- .../src/app/billing/settings/sponsored-families.component.html | 2 +- apps/web/src/locales/en/messages.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/web/src/app/billing/members/free-bitwarden-families.component.html b/apps/web/src/app/billing/members/free-bitwarden-families.component.html index 9e32fb925a8..8eeef6f0f36 100644 --- a/apps/web/src/app/billing/members/free-bitwarden-families.component.html +++ b/apps/web/src/app/billing/members/free-bitwarden-families.component.html @@ -14,7 +14,7 @@ {{ "sponsoredFamiliesIncludeMessage" | i18n }}:
      • {{ "sponsoredFamiliesPremiumAccess" | i18n }}
      • -
      • {{ "sponsoredFamiliesSharedCollectionsMessage" | i18n }}
      • +
      • {{ "sponsoredFamiliesSharedCollectionsForFamilyMembers" | i18n }}
      diff --git a/apps/web/src/app/billing/settings/sponsored-families.component.html b/apps/web/src/app/billing/settings/sponsored-families.component.html index 5a6957718a3..7708f63365e 100644 --- a/apps/web/src/app/billing/settings/sponsored-families.component.html +++ b/apps/web/src/app/billing/settings/sponsored-families.component.html @@ -13,7 +13,7 @@ {{ "sponsoredFamiliesIncludeMessage" | i18n }}:
      • {{ "sponsoredFamiliesPremiumAccess" | i18n }}
      • -
      • {{ "sponsoredFamiliesSharedCollectionsMessage" | i18n }}
      • +
      • {{ "sponsoredFamiliesSharedCollectionsForFamilyMembers" | i18n }}
      diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 59ba7961d82..8a37db4afda 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -6327,7 +6327,7 @@ "sponsoredFamiliesPremiumAccess": { "message": "Premium access for up to 6 users" }, - "sponsoredFamiliesSharedCollectionsMessage": { + "sponsoredFamiliesSharedCollectionsForFamilyMembers": { "message": "Shared collections for family members" }, "memberFamilies": { From 8fbb46a360b91387526a16665988aa9d472d4424 Mon Sep 17 00:00:00 2001 From: "bw-ghapp[bot]" <178206702+bw-ghapp[bot]@users.noreply.github.com> Date: Fri, 2 May 2025 15:24:49 +0200 Subject: [PATCH 119/499] Autosync the updated translations (#14592) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/browser/src/_locales/ar/messages.json | 53 +++++++++++++- apps/browser/src/_locales/az/messages.json | 53 +++++++++++++- apps/browser/src/_locales/be/messages.json | 53 +++++++++++++- apps/browser/src/_locales/bg/messages.json | 53 +++++++++++++- apps/browser/src/_locales/bn/messages.json | 53 +++++++++++++- apps/browser/src/_locales/bs/messages.json | 53 +++++++++++++- apps/browser/src/_locales/ca/messages.json | 53 +++++++++++++- apps/browser/src/_locales/cs/messages.json | 53 +++++++++++++- apps/browser/src/_locales/cy/messages.json | 53 +++++++++++++- apps/browser/src/_locales/da/messages.json | 53 +++++++++++++- apps/browser/src/_locales/de/messages.json | 53 +++++++++++++- apps/browser/src/_locales/el/messages.json | 53 +++++++++++++- apps/browser/src/_locales/en_GB/messages.json | 53 +++++++++++++- apps/browser/src/_locales/en_IN/messages.json | 53 +++++++++++++- apps/browser/src/_locales/es/messages.json | 53 +++++++++++++- apps/browser/src/_locales/et/messages.json | 53 +++++++++++++- apps/browser/src/_locales/eu/messages.json | 53 +++++++++++++- apps/browser/src/_locales/fa/messages.json | 53 +++++++++++++- apps/browser/src/_locales/fi/messages.json | 67 ++++++++++++++--- apps/browser/src/_locales/fil/messages.json | 53 +++++++++++++- apps/browser/src/_locales/fr/messages.json | 53 +++++++++++++- apps/browser/src/_locales/gl/messages.json | 53 +++++++++++++- apps/browser/src/_locales/he/messages.json | 53 +++++++++++++- apps/browser/src/_locales/hi/messages.json | 53 +++++++++++++- apps/browser/src/_locales/hr/messages.json | 53 +++++++++++++- apps/browser/src/_locales/hu/messages.json | 53 +++++++++++++- apps/browser/src/_locales/id/messages.json | 53 +++++++++++++- apps/browser/src/_locales/it/messages.json | 53 +++++++++++++- apps/browser/src/_locales/ja/messages.json | 53 +++++++++++++- apps/browser/src/_locales/ka/messages.json | 53 +++++++++++++- apps/browser/src/_locales/km/messages.json | 53 +++++++++++++- apps/browser/src/_locales/kn/messages.json | 53 +++++++++++++- apps/browser/src/_locales/ko/messages.json | 53 +++++++++++++- apps/browser/src/_locales/lt/messages.json | 53 +++++++++++++- apps/browser/src/_locales/lv/messages.json | 67 ++++++++++++++--- apps/browser/src/_locales/ml/messages.json | 53 +++++++++++++- apps/browser/src/_locales/mr/messages.json | 53 +++++++++++++- apps/browser/src/_locales/my/messages.json | 53 +++++++++++++- apps/browser/src/_locales/nb/messages.json | 55 +++++++++++++- apps/browser/src/_locales/ne/messages.json | 53 +++++++++++++- apps/browser/src/_locales/nl/messages.json | 53 +++++++++++++- apps/browser/src/_locales/nn/messages.json | 53 +++++++++++++- apps/browser/src/_locales/or/messages.json | 53 +++++++++++++- apps/browser/src/_locales/pl/messages.json | 53 +++++++++++++- apps/browser/src/_locales/pt_BR/messages.json | 53 +++++++++++++- apps/browser/src/_locales/pt_PT/messages.json | 53 +++++++++++++- apps/browser/src/_locales/ro/messages.json | 53 +++++++++++++- apps/browser/src/_locales/ru/messages.json | 53 +++++++++++++- apps/browser/src/_locales/si/messages.json | 53 +++++++++++++- apps/browser/src/_locales/sk/messages.json | 53 +++++++++++++- apps/browser/src/_locales/sl/messages.json | 53 +++++++++++++- apps/browser/src/_locales/sr/messages.json | 71 ++++++++++++++++--- apps/browser/src/_locales/sv/messages.json | 53 +++++++++++++- apps/browser/src/_locales/te/messages.json | 53 +++++++++++++- apps/browser/src/_locales/th/messages.json | 53 +++++++++++++- apps/browser/src/_locales/tr/messages.json | 53 +++++++++++++- apps/browser/src/_locales/uk/messages.json | 53 +++++++++++++- apps/browser/src/_locales/vi/messages.json | 53 +++++++++++++- apps/browser/src/_locales/zh_CN/messages.json | 55 +++++++++++++- apps/browser/src/_locales/zh_TW/messages.json | 53 +++++++++++++- 60 files changed, 3145 insertions(+), 85 deletions(-) diff --git a/apps/browser/src/_locales/ar/messages.json b/apps/browser/src/_locales/ar/messages.json index deef20f6a1f..c2f3c72f281 100644 --- a/apps/browser/src/_locales/ar/messages.json +++ b/apps/browser/src/_locales/ar/messages.json @@ -5201,6 +5201,12 @@ "changeAtRiskPassword": { "message": "Change at-risk password" }, + "settingsVaultOptions": { + "message": "Vault options" + }, + "emptyVaultDescription": { + "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + }, "introCarouselLabel": { "message": "Welcome to Bitwarden" }, @@ -5227,5 +5233,50 @@ }, "secureDevicesBody": { "message": "Save unlimited passwords across unlimited devices with Bitwarden mobile, browser, and desktop apps." + }, + "emptyVaultNudgeTitle": { + "message": "Import existing passwords" + }, + "emptyVaultNudgeBody": { + "message": "Use the importer to quickly transfer logins to Bitwarden without manually adding them." + }, + "emptyVaultNudgeButton": { + "message": "Import now" + }, + "hasItemsVaultNudgeTitle": { + "message": "Welcome to your vault!" + }, + "hasItemsVaultNudgeBody": { + "message": "Autofill items for the current page\nFavorite items for easy access\nSearch your vault for something else" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/az/messages.json b/apps/browser/src/_locales/az/messages.json index 1b7bec7367a..3550ec52462 100644 --- a/apps/browser/src/_locales/az/messages.json +++ b/apps/browser/src/_locales/az/messages.json @@ -5201,6 +5201,12 @@ "changeAtRiskPassword": { "message": "Riskli parolları dəyişdir" }, + "settingsVaultOptions": { + "message": "Vault options" + }, + "emptyVaultDescription": { + "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + }, "introCarouselLabel": { "message": "Bitwarden-ə xoş gəlmisiniz" }, @@ -5227,5 +5233,50 @@ }, "secureDevicesBody": { "message": "Bitwarden mobil, brauzer və masaüstü tətbiqləri ilə limitsiz cihaz arasında limitsiz parol saxlayın." + }, + "emptyVaultNudgeTitle": { + "message": "Import existing passwords" + }, + "emptyVaultNudgeBody": { + "message": "Use the importer to quickly transfer logins to Bitwarden without manually adding them." + }, + "emptyVaultNudgeButton": { + "message": "Import now" + }, + "hasItemsVaultNudgeTitle": { + "message": "Welcome to your vault!" + }, + "hasItemsVaultNudgeBody": { + "message": "Autofill items for the current page\nFavorite items for easy access\nSearch your vault for something else" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/be/messages.json b/apps/browser/src/_locales/be/messages.json index 132e1a75fe1..20c5bb8ff35 100644 --- a/apps/browser/src/_locales/be/messages.json +++ b/apps/browser/src/_locales/be/messages.json @@ -5201,6 +5201,12 @@ "changeAtRiskPassword": { "message": "Change at-risk password" }, + "settingsVaultOptions": { + "message": "Vault options" + }, + "emptyVaultDescription": { + "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + }, "introCarouselLabel": { "message": "Welcome to Bitwarden" }, @@ -5227,5 +5233,50 @@ }, "secureDevicesBody": { "message": "Save unlimited passwords across unlimited devices with Bitwarden mobile, browser, and desktop apps." + }, + "emptyVaultNudgeTitle": { + "message": "Import existing passwords" + }, + "emptyVaultNudgeBody": { + "message": "Use the importer to quickly transfer logins to Bitwarden without manually adding them." + }, + "emptyVaultNudgeButton": { + "message": "Import now" + }, + "hasItemsVaultNudgeTitle": { + "message": "Welcome to your vault!" + }, + "hasItemsVaultNudgeBody": { + "message": "Autofill items for the current page\nFavorite items for easy access\nSearch your vault for something else" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/bg/messages.json b/apps/browser/src/_locales/bg/messages.json index d54b9d710eb..2bc42c0e6c9 100644 --- a/apps/browser/src/_locales/bg/messages.json +++ b/apps/browser/src/_locales/bg/messages.json @@ -5201,6 +5201,12 @@ "changeAtRiskPassword": { "message": "Промяна на парола в риск" }, + "settingsVaultOptions": { + "message": "Настройки на трезора" + }, + "emptyVaultDescription": { + "message": "Трезорът може да пази не само паролите Ви. Съхранявайте защитени данни за вход, идентификационни данни, карти и бележки." + }, "introCarouselLabel": { "message": "Добре дошли в Битуорден" }, @@ -5227,5 +5233,50 @@ }, "secureDevicesBody": { "message": "Съхранявайте неограничен брой пароли на множество устройства – с приложенията на Битуорден за мобилни телефони, браузър и компютър." + }, + "emptyVaultNudgeTitle": { + "message": "Внасяне на съществуващи пароли" + }, + "emptyVaultNudgeBody": { + "message": "Използвайте функцията за внасяне, за да прехвърлите лесно данните си за вписване в Битуорден, без да ги добавяте ръчно." + }, + "emptyVaultNudgeButton": { + "message": "Внасяне сега" + }, + "hasItemsVaultNudgeTitle": { + "message": "Добре дошли в трезора си!" + }, + "hasItemsVaultNudgeBody": { + "message": "Елементи за авт. попълване в текущата страница\nЛюбими елементи за лесен достъп\nПотърсете в трезора си за нещо друго" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/bn/messages.json b/apps/browser/src/_locales/bn/messages.json index 90965cff8d6..c14d5308a6b 100644 --- a/apps/browser/src/_locales/bn/messages.json +++ b/apps/browser/src/_locales/bn/messages.json @@ -5201,6 +5201,12 @@ "changeAtRiskPassword": { "message": "Change at-risk password" }, + "settingsVaultOptions": { + "message": "Vault options" + }, + "emptyVaultDescription": { + "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + }, "introCarouselLabel": { "message": "Welcome to Bitwarden" }, @@ -5227,5 +5233,50 @@ }, "secureDevicesBody": { "message": "Save unlimited passwords across unlimited devices with Bitwarden mobile, browser, and desktop apps." + }, + "emptyVaultNudgeTitle": { + "message": "Import existing passwords" + }, + "emptyVaultNudgeBody": { + "message": "Use the importer to quickly transfer logins to Bitwarden without manually adding them." + }, + "emptyVaultNudgeButton": { + "message": "Import now" + }, + "hasItemsVaultNudgeTitle": { + "message": "Welcome to your vault!" + }, + "hasItemsVaultNudgeBody": { + "message": "Autofill items for the current page\nFavorite items for easy access\nSearch your vault for something else" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/bs/messages.json b/apps/browser/src/_locales/bs/messages.json index cb845575ae8..eaa9ba863e0 100644 --- a/apps/browser/src/_locales/bs/messages.json +++ b/apps/browser/src/_locales/bs/messages.json @@ -5201,6 +5201,12 @@ "changeAtRiskPassword": { "message": "Change at-risk password" }, + "settingsVaultOptions": { + "message": "Vault options" + }, + "emptyVaultDescription": { + "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + }, "introCarouselLabel": { "message": "Welcome to Bitwarden" }, @@ -5227,5 +5233,50 @@ }, "secureDevicesBody": { "message": "Save unlimited passwords across unlimited devices with Bitwarden mobile, browser, and desktop apps." + }, + "emptyVaultNudgeTitle": { + "message": "Import existing passwords" + }, + "emptyVaultNudgeBody": { + "message": "Use the importer to quickly transfer logins to Bitwarden without manually adding them." + }, + "emptyVaultNudgeButton": { + "message": "Import now" + }, + "hasItemsVaultNudgeTitle": { + "message": "Welcome to your vault!" + }, + "hasItemsVaultNudgeBody": { + "message": "Autofill items for the current page\nFavorite items for easy access\nSearch your vault for something else" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/ca/messages.json b/apps/browser/src/_locales/ca/messages.json index 4394488f189..bbf6ccb9c41 100644 --- a/apps/browser/src/_locales/ca/messages.json +++ b/apps/browser/src/_locales/ca/messages.json @@ -5201,6 +5201,12 @@ "changeAtRiskPassword": { "message": "Change at-risk password" }, + "settingsVaultOptions": { + "message": "Vault options" + }, + "emptyVaultDescription": { + "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + }, "introCarouselLabel": { "message": "Welcome to Bitwarden" }, @@ -5227,5 +5233,50 @@ }, "secureDevicesBody": { "message": "Save unlimited passwords across unlimited devices with Bitwarden mobile, browser, and desktop apps." + }, + "emptyVaultNudgeTitle": { + "message": "Import existing passwords" + }, + "emptyVaultNudgeBody": { + "message": "Use the importer to quickly transfer logins to Bitwarden without manually adding them." + }, + "emptyVaultNudgeButton": { + "message": "Import now" + }, + "hasItemsVaultNudgeTitle": { + "message": "Welcome to your vault!" + }, + "hasItemsVaultNudgeBody": { + "message": "Autofill items for the current page\nFavorite items for easy access\nSearch your vault for something else" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/cs/messages.json b/apps/browser/src/_locales/cs/messages.json index 2e0d015387a..dfbc0acea76 100644 --- a/apps/browser/src/_locales/cs/messages.json +++ b/apps/browser/src/_locales/cs/messages.json @@ -5201,6 +5201,12 @@ "changeAtRiskPassword": { "message": "Změnit ohrožené heslo" }, + "settingsVaultOptions": { + "message": "Volby trezoru" + }, + "emptyVaultDescription": { + "message": "Trezor chrání více než jen Vaše hesla. Bezpečně zde uložte zabezpečená přihlášení, ID, karty a poznámky." + }, "introCarouselLabel": { "message": "Vítejte v Bitwardenu" }, @@ -5227,5 +5233,50 @@ }, "secureDevicesBody": { "message": "Uložte neomezená hesla na neomezených zařízeních s Bitwardenem na mobilu, prohlížeči a desktopové aplikaci." + }, + "emptyVaultNudgeTitle": { + "message": "Importovat existující hesla" + }, + "emptyVaultNudgeBody": { + "message": "Pomocí importu rychle přenesete přihlašovací údaje do Bitwardenu a to bez jejich ručního přidání." + }, + "emptyVaultNudgeButton": { + "message": "Importovat nyní" + }, + "hasItemsVaultNudgeTitle": { + "message": "Vítejte ve Vašem trezoru!" + }, + "hasItemsVaultNudgeBody": { + "message": "Položky automatického vyplňování pro aktuální stránku\nOblíbené položky pro snadný přístup\nNajděte v trezoru něco jiného" + }, + "newLoginNudgeTitle": { + "message": "Ušetřete čas s automatickým vyplňováním" + }, + "newLoginNudgeBody": { + "message": "Zahrne webovou stránku, takže se toto přihlášení objeví jako návrh automatického vyplňování." + }, + "newCardNudgeTitle": { + "message": "Bezproblémová online pokladna" + }, + "newCardNudgeBody": { + "message": "Karty - snadné, bezpečné a přesné vyplňování platebních formulářů." + }, + "newIdentityNudgeTitle": { + "message": "Jednodušší vytváření účtů" + }, + "newIdentityNudgeBody": { + "message": "Identity - rychlé automatické vyplňování dlouhých registračních nebo kontaktních formulářů." + }, + "newNoteNudgeTitle": { + "message": "Udržujte svá citlivá data v bezpečí" + }, + "newNoteNudgeBody": { + "message": "Poznámky - bezpečné ukládání citlivých údajů, jako jsou bankovní nebo pojišťovací údaje." + }, + "newSshNudgeTitle": { + "message": "Přístup SSH pro vývojáře" + }, + "newSshNudgeBody": { + "message": "Uložte své klíče a připojte se k SSH agentovi pro rychlé a šifrované ověření." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/cy/messages.json b/apps/browser/src/_locales/cy/messages.json index fdde829ed61..5fa135a929d 100644 --- a/apps/browser/src/_locales/cy/messages.json +++ b/apps/browser/src/_locales/cy/messages.json @@ -5201,6 +5201,12 @@ "changeAtRiskPassword": { "message": "Change at-risk password" }, + "settingsVaultOptions": { + "message": "Vault options" + }, + "emptyVaultDescription": { + "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + }, "introCarouselLabel": { "message": "Welcome to Bitwarden" }, @@ -5227,5 +5233,50 @@ }, "secureDevicesBody": { "message": "Save unlimited passwords across unlimited devices with Bitwarden mobile, browser, and desktop apps." + }, + "emptyVaultNudgeTitle": { + "message": "Import existing passwords" + }, + "emptyVaultNudgeBody": { + "message": "Use the importer to quickly transfer logins to Bitwarden without manually adding them." + }, + "emptyVaultNudgeButton": { + "message": "Import now" + }, + "hasItemsVaultNudgeTitle": { + "message": "Welcome to your vault!" + }, + "hasItemsVaultNudgeBody": { + "message": "Autofill items for the current page\nFavorite items for easy access\nSearch your vault for something else" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/da/messages.json b/apps/browser/src/_locales/da/messages.json index afce44f6bc2..f1c55f3557d 100644 --- a/apps/browser/src/_locales/da/messages.json +++ b/apps/browser/src/_locales/da/messages.json @@ -5201,6 +5201,12 @@ "changeAtRiskPassword": { "message": "Change at-risk password" }, + "settingsVaultOptions": { + "message": "Vault options" + }, + "emptyVaultDescription": { + "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + }, "introCarouselLabel": { "message": "Welcome to Bitwarden" }, @@ -5227,5 +5233,50 @@ }, "secureDevicesBody": { "message": "Save unlimited passwords across unlimited devices with Bitwarden mobile, browser, and desktop apps." + }, + "emptyVaultNudgeTitle": { + "message": "Import existing passwords" + }, + "emptyVaultNudgeBody": { + "message": "Use the importer to quickly transfer logins to Bitwarden without manually adding them." + }, + "emptyVaultNudgeButton": { + "message": "Import now" + }, + "hasItemsVaultNudgeTitle": { + "message": "Welcome to your vault!" + }, + "hasItemsVaultNudgeBody": { + "message": "Autofill items for the current page\nFavorite items for easy access\nSearch your vault for something else" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/de/messages.json b/apps/browser/src/_locales/de/messages.json index 242f3c57392..251a0323c60 100644 --- a/apps/browser/src/_locales/de/messages.json +++ b/apps/browser/src/_locales/de/messages.json @@ -5201,6 +5201,12 @@ "changeAtRiskPassword": { "message": "Gefährdetes Passwort ändern" }, + "settingsVaultOptions": { + "message": "Vault options" + }, + "emptyVaultDescription": { + "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + }, "introCarouselLabel": { "message": "Willkommen bei Bitwarden" }, @@ -5227,5 +5233,50 @@ }, "secureDevicesBody": { "message": "Speicher eine unbegrenzte Anzahl von Passwörtern auf unbegrenzt vielen Geräten mit Bitwarden-Apps für Smartphones, Browser und Desktop." + }, + "emptyVaultNudgeTitle": { + "message": "Import existing passwords" + }, + "emptyVaultNudgeBody": { + "message": "Use the importer to quickly transfer logins to Bitwarden without manually adding them." + }, + "emptyVaultNudgeButton": { + "message": "Import now" + }, + "hasItemsVaultNudgeTitle": { + "message": "Welcome to your vault!" + }, + "hasItemsVaultNudgeBody": { + "message": "Autofill items for the current page\nFavorite items for easy access\nSearch your vault for something else" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/el/messages.json b/apps/browser/src/_locales/el/messages.json index d9a1e088b2d..691f546b327 100644 --- a/apps/browser/src/_locales/el/messages.json +++ b/apps/browser/src/_locales/el/messages.json @@ -5201,6 +5201,12 @@ "changeAtRiskPassword": { "message": "Change at-risk password" }, + "settingsVaultOptions": { + "message": "Vault options" + }, + "emptyVaultDescription": { + "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + }, "introCarouselLabel": { "message": "Welcome to Bitwarden" }, @@ -5227,5 +5233,50 @@ }, "secureDevicesBody": { "message": "Save unlimited passwords across unlimited devices with Bitwarden mobile, browser, and desktop apps." + }, + "emptyVaultNudgeTitle": { + "message": "Import existing passwords" + }, + "emptyVaultNudgeBody": { + "message": "Use the importer to quickly transfer logins to Bitwarden without manually adding them." + }, + "emptyVaultNudgeButton": { + "message": "Import now" + }, + "hasItemsVaultNudgeTitle": { + "message": "Welcome to your vault!" + }, + "hasItemsVaultNudgeBody": { + "message": "Autofill items for the current page\nFavorite items for easy access\nSearch your vault for something else" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/en_GB/messages.json b/apps/browser/src/_locales/en_GB/messages.json index f96f1016975..1e0def93584 100644 --- a/apps/browser/src/_locales/en_GB/messages.json +++ b/apps/browser/src/_locales/en_GB/messages.json @@ -5201,6 +5201,12 @@ "changeAtRiskPassword": { "message": "Change at-risk password" }, + "settingsVaultOptions": { + "message": "Vault options" + }, + "emptyVaultDescription": { + "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + }, "introCarouselLabel": { "message": "Welcome to Bitwarden" }, @@ -5227,5 +5233,50 @@ }, "secureDevicesBody": { "message": "Save unlimited passwords across unlimited devices with Bitwarden mobile, browser, and desktop apps." + }, + "emptyVaultNudgeTitle": { + "message": "Import existing passwords" + }, + "emptyVaultNudgeBody": { + "message": "Use the importer to quickly transfer logins to Bitwarden without manually adding them." + }, + "emptyVaultNudgeButton": { + "message": "Import now" + }, + "hasItemsVaultNudgeTitle": { + "message": "Welcome to your vault!" + }, + "hasItemsVaultNudgeBody": { + "message": "Autofill items for the current page\nFavourite items for easy access\nSearch your vault for something else" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/en_IN/messages.json b/apps/browser/src/_locales/en_IN/messages.json index 48c63c615f3..380eab31eda 100644 --- a/apps/browser/src/_locales/en_IN/messages.json +++ b/apps/browser/src/_locales/en_IN/messages.json @@ -5201,6 +5201,12 @@ "changeAtRiskPassword": { "message": "Change at-risk password" }, + "settingsVaultOptions": { + "message": "Vault options" + }, + "emptyVaultDescription": { + "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + }, "introCarouselLabel": { "message": "Welcome to Bitwarden" }, @@ -5227,5 +5233,50 @@ }, "secureDevicesBody": { "message": "Save unlimited passwords across unlimited devices with Bitwarden mobile, browser, and desktop apps." + }, + "emptyVaultNudgeTitle": { + "message": "Import existing passwords" + }, + "emptyVaultNudgeBody": { + "message": "Use the importer to quickly transfer logins to Bitwarden without manually adding them." + }, + "emptyVaultNudgeButton": { + "message": "Import now" + }, + "hasItemsVaultNudgeTitle": { + "message": "Welcome to your vault!" + }, + "hasItemsVaultNudgeBody": { + "message": "Autofill items for the current page\nFavourite items for easy access\nSearch your vault for something else" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/es/messages.json b/apps/browser/src/_locales/es/messages.json index 900c03d8963..76401c685ad 100644 --- a/apps/browser/src/_locales/es/messages.json +++ b/apps/browser/src/_locales/es/messages.json @@ -5201,6 +5201,12 @@ "changeAtRiskPassword": { "message": "Cambiar contraseña de riesgo" }, + "settingsVaultOptions": { + "message": "Vault options" + }, + "emptyVaultDescription": { + "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + }, "introCarouselLabel": { "message": "Welcome to Bitwarden" }, @@ -5227,5 +5233,50 @@ }, "secureDevicesBody": { "message": "Guarda contraseñas ilimitadas a través de dispositivos ilimitados con aplicaciones móviles, de navegador y de escritorio de Bitwarden." + }, + "emptyVaultNudgeTitle": { + "message": "Import existing passwords" + }, + "emptyVaultNudgeBody": { + "message": "Use the importer to quickly transfer logins to Bitwarden without manually adding them." + }, + "emptyVaultNudgeButton": { + "message": "Import now" + }, + "hasItemsVaultNudgeTitle": { + "message": "Welcome to your vault!" + }, + "hasItemsVaultNudgeBody": { + "message": "Autofill items for the current page\nFavorite items for easy access\nSearch your vault for something else" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/et/messages.json b/apps/browser/src/_locales/et/messages.json index ff7b222202b..f76adcc27e2 100644 --- a/apps/browser/src/_locales/et/messages.json +++ b/apps/browser/src/_locales/et/messages.json @@ -5201,6 +5201,12 @@ "changeAtRiskPassword": { "message": "Change at-risk password" }, + "settingsVaultOptions": { + "message": "Vault options" + }, + "emptyVaultDescription": { + "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + }, "introCarouselLabel": { "message": "Welcome to Bitwarden" }, @@ -5227,5 +5233,50 @@ }, "secureDevicesBody": { "message": "Save unlimited passwords across unlimited devices with Bitwarden mobile, browser, and desktop apps." + }, + "emptyVaultNudgeTitle": { + "message": "Import existing passwords" + }, + "emptyVaultNudgeBody": { + "message": "Use the importer to quickly transfer logins to Bitwarden without manually adding them." + }, + "emptyVaultNudgeButton": { + "message": "Import now" + }, + "hasItemsVaultNudgeTitle": { + "message": "Welcome to your vault!" + }, + "hasItemsVaultNudgeBody": { + "message": "Autofill items for the current page\nFavorite items for easy access\nSearch your vault for something else" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/eu/messages.json b/apps/browser/src/_locales/eu/messages.json index c87f87dbc22..cd3d58896d4 100644 --- a/apps/browser/src/_locales/eu/messages.json +++ b/apps/browser/src/_locales/eu/messages.json @@ -5201,6 +5201,12 @@ "changeAtRiskPassword": { "message": "Change at-risk password" }, + "settingsVaultOptions": { + "message": "Vault options" + }, + "emptyVaultDescription": { + "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + }, "introCarouselLabel": { "message": "Welcome to Bitwarden" }, @@ -5227,5 +5233,50 @@ }, "secureDevicesBody": { "message": "Save unlimited passwords across unlimited devices with Bitwarden mobile, browser, and desktop apps." + }, + "emptyVaultNudgeTitle": { + "message": "Import existing passwords" + }, + "emptyVaultNudgeBody": { + "message": "Use the importer to quickly transfer logins to Bitwarden without manually adding them." + }, + "emptyVaultNudgeButton": { + "message": "Import now" + }, + "hasItemsVaultNudgeTitle": { + "message": "Welcome to your vault!" + }, + "hasItemsVaultNudgeBody": { + "message": "Autofill items for the current page\nFavorite items for easy access\nSearch your vault for something else" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/fa/messages.json b/apps/browser/src/_locales/fa/messages.json index 656a3e6b609..b8f8d2ccdbf 100644 --- a/apps/browser/src/_locales/fa/messages.json +++ b/apps/browser/src/_locales/fa/messages.json @@ -5201,6 +5201,12 @@ "changeAtRiskPassword": { "message": "Change at-risk password" }, + "settingsVaultOptions": { + "message": "Vault options" + }, + "emptyVaultDescription": { + "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + }, "introCarouselLabel": { "message": "Welcome to Bitwarden" }, @@ -5227,5 +5233,50 @@ }, "secureDevicesBody": { "message": "Save unlimited passwords across unlimited devices with Bitwarden mobile, browser, and desktop apps." + }, + "emptyVaultNudgeTitle": { + "message": "Import existing passwords" + }, + "emptyVaultNudgeBody": { + "message": "Use the importer to quickly transfer logins to Bitwarden without manually adding them." + }, + "emptyVaultNudgeButton": { + "message": "Import now" + }, + "hasItemsVaultNudgeTitle": { + "message": "Welcome to your vault!" + }, + "hasItemsVaultNudgeBody": { + "message": "Autofill items for the current page\nFavorite items for easy access\nSearch your vault for something else" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/fi/messages.json b/apps/browser/src/_locales/fi/messages.json index e0da64cd0b8..c3f48b1df3b 100644 --- a/apps/browser/src/_locales/fi/messages.json +++ b/apps/browser/src/_locales/fi/messages.json @@ -1072,14 +1072,14 @@ "description": "Aria label for the view button in notification bar confirmation message" }, "notificationEditTooltip": { - "message": "Edit before saving", + "message": "Muokkaa ennen tallentamista", "description": "Tooltip and Aria label for edit button on cipher item" }, "newNotification": { - "message": "New notification" + "message": "Uusi ilmoitus" }, "labelWithNotification": { - "message": "$LABEL$: New notification", + "message": "$LABEL$: Uusi ilmoitus", "description": "Label for the notification with a new login suggestion.", "placeholders": { "label": { @@ -1089,7 +1089,7 @@ } }, "loginSaveConfirmation": { - "message": "$ITEMNAME$ saved to Bitwarden.", + "message": "$ITEMNAME$ tallennettiin Bitwardeniin.", "placeholders": { "itemName": { "content": "$1" @@ -1098,7 +1098,7 @@ "description": "Shown to user after item is saved." }, "loginUpdatedConfirmation": { - "message": "$ITEMNAME$ updated in Bitwarden.", + "message": "$ITEMNAME$ päivitettiin Bitwardeniin.", "placeholders": { "itemName": { "content": "$1" @@ -1115,11 +1115,11 @@ "description": "Button text for updating an existing login entry." }, "saveLogin": { - "message": "Save login", + "message": "Tallenna kirjautumistieto", "description": "Prompt asking the user if they want to save their login details." }, "updateLogin": { - "message": "Update existing login", + "message": "Päivitä olemassaoleva kirjautumistieto", "description": "Prompt asking the user if they want to update an existing login entry." }, "loginSaveSuccess": { @@ -5201,6 +5201,12 @@ "changeAtRiskPassword": { "message": "Vaihda vaarantunut salasana" }, + "settingsVaultOptions": { + "message": "Vault options" + }, + "emptyVaultDescription": { + "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + }, "introCarouselLabel": { "message": "Tervetuloa Bitwardeniin" }, @@ -5227,5 +5233,50 @@ }, "secureDevicesBody": { "message": "Tallenna rajattomasti salasanoja, rajattomalla määrällä laitteita, Bitwardenin mobiili-, selain- ja työpöytäsovelluksilla." + }, + "emptyVaultNudgeTitle": { + "message": "Import existing passwords" + }, + "emptyVaultNudgeBody": { + "message": "Use the importer to quickly transfer logins to Bitwarden without manually adding them." + }, + "emptyVaultNudgeButton": { + "message": "Import now" + }, + "hasItemsVaultNudgeTitle": { + "message": "Welcome to your vault!" + }, + "hasItemsVaultNudgeBody": { + "message": "Autofill items for the current page\nFavorite items for easy access\nSearch your vault for something else" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/fil/messages.json b/apps/browser/src/_locales/fil/messages.json index 2f1a6b3a9c9..d01670a2353 100644 --- a/apps/browser/src/_locales/fil/messages.json +++ b/apps/browser/src/_locales/fil/messages.json @@ -5201,6 +5201,12 @@ "changeAtRiskPassword": { "message": "Change at-risk password" }, + "settingsVaultOptions": { + "message": "Vault options" + }, + "emptyVaultDescription": { + "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + }, "introCarouselLabel": { "message": "Welcome to Bitwarden" }, @@ -5227,5 +5233,50 @@ }, "secureDevicesBody": { "message": "Save unlimited passwords across unlimited devices with Bitwarden mobile, browser, and desktop apps." + }, + "emptyVaultNudgeTitle": { + "message": "Import existing passwords" + }, + "emptyVaultNudgeBody": { + "message": "Use the importer to quickly transfer logins to Bitwarden without manually adding them." + }, + "emptyVaultNudgeButton": { + "message": "Import now" + }, + "hasItemsVaultNudgeTitle": { + "message": "Welcome to your vault!" + }, + "hasItemsVaultNudgeBody": { + "message": "Autofill items for the current page\nFavorite items for easy access\nSearch your vault for something else" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/fr/messages.json b/apps/browser/src/_locales/fr/messages.json index 696b9a75d05..b8c4fa9d011 100644 --- a/apps/browser/src/_locales/fr/messages.json +++ b/apps/browser/src/_locales/fr/messages.json @@ -5201,6 +5201,12 @@ "changeAtRiskPassword": { "message": "Changer le mot de passe à risque" }, + "settingsVaultOptions": { + "message": "Vault options" + }, + "emptyVaultDescription": { + "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + }, "introCarouselLabel": { "message": "Welcome to Bitwarden" }, @@ -5227,5 +5233,50 @@ }, "secureDevicesBody": { "message": "Save unlimited passwords across unlimited devices with Bitwarden mobile, browser, and desktop apps." + }, + "emptyVaultNudgeTitle": { + "message": "Import existing passwords" + }, + "emptyVaultNudgeBody": { + "message": "Use the importer to quickly transfer logins to Bitwarden without manually adding them." + }, + "emptyVaultNudgeButton": { + "message": "Import now" + }, + "hasItemsVaultNudgeTitle": { + "message": "Welcome to your vault!" + }, + "hasItemsVaultNudgeBody": { + "message": "Autofill items for the current page\nFavorite items for easy access\nSearch your vault for something else" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/gl/messages.json b/apps/browser/src/_locales/gl/messages.json index 329de4ae2be..40823d54799 100644 --- a/apps/browser/src/_locales/gl/messages.json +++ b/apps/browser/src/_locales/gl/messages.json @@ -5201,6 +5201,12 @@ "changeAtRiskPassword": { "message": "Change at-risk password" }, + "settingsVaultOptions": { + "message": "Vault options" + }, + "emptyVaultDescription": { + "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + }, "introCarouselLabel": { "message": "Welcome to Bitwarden" }, @@ -5227,5 +5233,50 @@ }, "secureDevicesBody": { "message": "Save unlimited passwords across unlimited devices with Bitwarden mobile, browser, and desktop apps." + }, + "emptyVaultNudgeTitle": { + "message": "Import existing passwords" + }, + "emptyVaultNudgeBody": { + "message": "Use the importer to quickly transfer logins to Bitwarden without manually adding them." + }, + "emptyVaultNudgeButton": { + "message": "Import now" + }, + "hasItemsVaultNudgeTitle": { + "message": "Welcome to your vault!" + }, + "hasItemsVaultNudgeBody": { + "message": "Autofill items for the current page\nFavorite items for easy access\nSearch your vault for something else" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/he/messages.json b/apps/browser/src/_locales/he/messages.json index 607875ae38e..a41386d9494 100644 --- a/apps/browser/src/_locales/he/messages.json +++ b/apps/browser/src/_locales/he/messages.json @@ -5201,6 +5201,12 @@ "changeAtRiskPassword": { "message": "שנה סיסמה בסיכון" }, + "settingsVaultOptions": { + "message": "Vault options" + }, + "emptyVaultDescription": { + "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + }, "introCarouselLabel": { "message": "Welcome to Bitwarden" }, @@ -5227,5 +5233,50 @@ }, "secureDevicesBody": { "message": "Save unlimited passwords across unlimited devices with Bitwarden mobile, browser, and desktop apps." + }, + "emptyVaultNudgeTitle": { + "message": "Import existing passwords" + }, + "emptyVaultNudgeBody": { + "message": "Use the importer to quickly transfer logins to Bitwarden without manually adding them." + }, + "emptyVaultNudgeButton": { + "message": "Import now" + }, + "hasItemsVaultNudgeTitle": { + "message": "Welcome to your vault!" + }, + "hasItemsVaultNudgeBody": { + "message": "Autofill items for the current page\nFavorite items for easy access\nSearch your vault for something else" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/hi/messages.json b/apps/browser/src/_locales/hi/messages.json index 39a3956654a..d4fff520b87 100644 --- a/apps/browser/src/_locales/hi/messages.json +++ b/apps/browser/src/_locales/hi/messages.json @@ -5201,6 +5201,12 @@ "changeAtRiskPassword": { "message": "Change at-risk password" }, + "settingsVaultOptions": { + "message": "Vault options" + }, + "emptyVaultDescription": { + "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + }, "introCarouselLabel": { "message": "Welcome to Bitwarden" }, @@ -5227,5 +5233,50 @@ }, "secureDevicesBody": { "message": "Save unlimited passwords across unlimited devices with Bitwarden mobile, browser, and desktop apps." + }, + "emptyVaultNudgeTitle": { + "message": "Import existing passwords" + }, + "emptyVaultNudgeBody": { + "message": "Use the importer to quickly transfer logins to Bitwarden without manually adding them." + }, + "emptyVaultNudgeButton": { + "message": "Import now" + }, + "hasItemsVaultNudgeTitle": { + "message": "Welcome to your vault!" + }, + "hasItemsVaultNudgeBody": { + "message": "Autofill items for the current page\nFavorite items for easy access\nSearch your vault for something else" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/hr/messages.json b/apps/browser/src/_locales/hr/messages.json index 30d314d2cbd..f3e4bd840c7 100644 --- a/apps/browser/src/_locales/hr/messages.json +++ b/apps/browser/src/_locales/hr/messages.json @@ -5201,6 +5201,12 @@ "changeAtRiskPassword": { "message": "Promijeni rizičnu lozinku" }, + "settingsVaultOptions": { + "message": "Vault options" + }, + "emptyVaultDescription": { + "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + }, "introCarouselLabel": { "message": "Welcome to Bitwarden" }, @@ -5227,5 +5233,50 @@ }, "secureDevicesBody": { "message": "Save unlimited passwords across unlimited devices with Bitwarden mobile, browser, and desktop apps." + }, + "emptyVaultNudgeTitle": { + "message": "Import existing passwords" + }, + "emptyVaultNudgeBody": { + "message": "Use the importer to quickly transfer logins to Bitwarden without manually adding them." + }, + "emptyVaultNudgeButton": { + "message": "Import now" + }, + "hasItemsVaultNudgeTitle": { + "message": "Welcome to your vault!" + }, + "hasItemsVaultNudgeBody": { + "message": "Autofill items for the current page\nFavorite items for easy access\nSearch your vault for something else" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/hu/messages.json b/apps/browser/src/_locales/hu/messages.json index 1fcedb0f9db..07161af5727 100644 --- a/apps/browser/src/_locales/hu/messages.json +++ b/apps/browser/src/_locales/hu/messages.json @@ -5201,6 +5201,12 @@ "changeAtRiskPassword": { "message": "Kockázatos jelszó megváltoztatása" }, + "settingsVaultOptions": { + "message": "Széf opciók" + }, + "emptyVaultDescription": { + "message": "A széf többre alkalmas, mint a jelszavak mentése. Menthetünk belépéseket, azonosítókat, kártyákat és feljegyzéseket teljes biztonságban." + }, "introCarouselLabel": { "message": "Üdvözlet a Bitwardenben" }, @@ -5227,5 +5233,50 @@ }, "secureDevicesBody": { "message": "Mentsünk el a korlátlan jelszót korlátlan számú eszközön a Bitwarden mobil, böngésző és asztali alkalmazásokkal." + }, + "emptyVaultNudgeTitle": { + "message": "Létező jelszavak importálása" + }, + "emptyVaultNudgeBody": { + "message": "Az importálóval gyorsan átvihetünk bejelentkezéseket a Bitwardenbe anélkül, hogy manuálisan hozzáadnánk azokat." + }, + "emptyVaultNudgeButton": { + "message": "Importálás most" + }, + "hasItemsVaultNudgeTitle": { + "message": "Üdvözlet a széfben!" + }, + "hasItemsVaultNudgeBody": { + "message": "Az aktuális oldal elemeinek automatikus kitöltése\nKedvenc elemek a könnyű hozzáférés érdekében\nValami más keresése a széfben" + }, + "newLoginNudgeTitle": { + "message": "Idő megtakarítás automatikus kitöltéssel" + }, + "newLoginNudgeBody": { + "message": "Adjunk meg egy webhelyet, hogy ez a bejelentkezési név automatikusan kitöltendő javaslatként jelenjen meg." + }, + "newCardNudgeTitle": { + "message": "Zökkenőmentes online fizetés" + }, + "newCardNudgeBody": { + "message": "Kártyákkal könnyedén, biztonságosan és pontosan tölthetjük ki automatikusan a fizetési űrlapokat." + }, + "newIdentityNudgeTitle": { + "message": "Egyszerűsítsük a fiókok létrehozását" + }, + "newIdentityNudgeBody": { + "message": "Azonosítókkal gyorsan automatikusan kitölthetjük a hosszú regisztrációs vagy kapcsolatfelvételi űrlapokat." + }, + "newNoteNudgeTitle": { + "message": "Tartsuk biztonságban az érzékeny adatokat" + }, + "newNoteNudgeBody": { + "message": "Jegyzetekkel biztonságosan tárolhatjuk az érzékeny adatokat, például a banki vagy biztosítási adatokat." + }, + "newSshNudgeTitle": { + "message": "Fejlesztőbarát SSH hozzáférés" + }, + "newSshNudgeBody": { + "message": "Tároljuk a kulcsokat és csatlakozzunk az SSH-ügynökhöz a gyors, titkosított hitelesítéshez." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/id/messages.json b/apps/browser/src/_locales/id/messages.json index 2e8a19ce28e..5c77c54ecf4 100644 --- a/apps/browser/src/_locales/id/messages.json +++ b/apps/browser/src/_locales/id/messages.json @@ -5201,6 +5201,12 @@ "changeAtRiskPassword": { "message": "Ubah kata sandi yang berrisiko" }, + "settingsVaultOptions": { + "message": "Vault options" + }, + "emptyVaultDescription": { + "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + }, "introCarouselLabel": { "message": "Welcome to Bitwarden" }, @@ -5227,5 +5233,50 @@ }, "secureDevicesBody": { "message": "Save unlimited passwords across unlimited devices with Bitwarden mobile, browser, and desktop apps." + }, + "emptyVaultNudgeTitle": { + "message": "Import existing passwords" + }, + "emptyVaultNudgeBody": { + "message": "Use the importer to quickly transfer logins to Bitwarden without manually adding them." + }, + "emptyVaultNudgeButton": { + "message": "Import now" + }, + "hasItemsVaultNudgeTitle": { + "message": "Welcome to your vault!" + }, + "hasItemsVaultNudgeBody": { + "message": "Autofill items for the current page\nFavorite items for easy access\nSearch your vault for something else" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/it/messages.json b/apps/browser/src/_locales/it/messages.json index 498ba22cd26..724416cdcde 100644 --- a/apps/browser/src/_locales/it/messages.json +++ b/apps/browser/src/_locales/it/messages.json @@ -5201,6 +5201,12 @@ "changeAtRiskPassword": { "message": "Cambia parola d'accesso a rischio" }, + "settingsVaultOptions": { + "message": "Vault options" + }, + "emptyVaultDescription": { + "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + }, "introCarouselLabel": { "message": "Welcome to Bitwarden" }, @@ -5227,5 +5233,50 @@ }, "secureDevicesBody": { "message": "Save unlimited passwords across unlimited devices with Bitwarden mobile, browser, and desktop apps." + }, + "emptyVaultNudgeTitle": { + "message": "Import existing passwords" + }, + "emptyVaultNudgeBody": { + "message": "Use the importer to quickly transfer logins to Bitwarden without manually adding them." + }, + "emptyVaultNudgeButton": { + "message": "Import now" + }, + "hasItemsVaultNudgeTitle": { + "message": "Welcome to your vault!" + }, + "hasItemsVaultNudgeBody": { + "message": "Autofill items for the current page\nFavorite items for easy access\nSearch your vault for something else" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/ja/messages.json b/apps/browser/src/_locales/ja/messages.json index 00cd29e8830..be70cd7d939 100644 --- a/apps/browser/src/_locales/ja/messages.json +++ b/apps/browser/src/_locales/ja/messages.json @@ -5201,6 +5201,12 @@ "changeAtRiskPassword": { "message": "危険なパスワードの変更" }, + "settingsVaultOptions": { + "message": "Vault options" + }, + "emptyVaultDescription": { + "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + }, "introCarouselLabel": { "message": "Bitwarden へようこそ" }, @@ -5227,5 +5233,50 @@ }, "secureDevicesBody": { "message": "Bitwarden のモバイル、ブラウザ、デスクトップアプリでは、保存できるパスワード数やデバイス数に制限はありません。" + }, + "emptyVaultNudgeTitle": { + "message": "Import existing passwords" + }, + "emptyVaultNudgeBody": { + "message": "Use the importer to quickly transfer logins to Bitwarden without manually adding them." + }, + "emptyVaultNudgeButton": { + "message": "Import now" + }, + "hasItemsVaultNudgeTitle": { + "message": "Welcome to your vault!" + }, + "hasItemsVaultNudgeBody": { + "message": "Autofill items for the current page\nFavorite items for easy access\nSearch your vault for something else" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/ka/messages.json b/apps/browser/src/_locales/ka/messages.json index 4c798e2b119..bb4a9be2a6c 100644 --- a/apps/browser/src/_locales/ka/messages.json +++ b/apps/browser/src/_locales/ka/messages.json @@ -5201,6 +5201,12 @@ "changeAtRiskPassword": { "message": "Change at-risk password" }, + "settingsVaultOptions": { + "message": "Vault options" + }, + "emptyVaultDescription": { + "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + }, "introCarouselLabel": { "message": "Welcome to Bitwarden" }, @@ -5227,5 +5233,50 @@ }, "secureDevicesBody": { "message": "Save unlimited passwords across unlimited devices with Bitwarden mobile, browser, and desktop apps." + }, + "emptyVaultNudgeTitle": { + "message": "Import existing passwords" + }, + "emptyVaultNudgeBody": { + "message": "Use the importer to quickly transfer logins to Bitwarden without manually adding them." + }, + "emptyVaultNudgeButton": { + "message": "Import now" + }, + "hasItemsVaultNudgeTitle": { + "message": "Welcome to your vault!" + }, + "hasItemsVaultNudgeBody": { + "message": "Autofill items for the current page\nFavorite items for easy access\nSearch your vault for something else" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/km/messages.json b/apps/browser/src/_locales/km/messages.json index 4f83b07506b..99a09c75d41 100644 --- a/apps/browser/src/_locales/km/messages.json +++ b/apps/browser/src/_locales/km/messages.json @@ -5201,6 +5201,12 @@ "changeAtRiskPassword": { "message": "Change at-risk password" }, + "settingsVaultOptions": { + "message": "Vault options" + }, + "emptyVaultDescription": { + "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + }, "introCarouselLabel": { "message": "Welcome to Bitwarden" }, @@ -5227,5 +5233,50 @@ }, "secureDevicesBody": { "message": "Save unlimited passwords across unlimited devices with Bitwarden mobile, browser, and desktop apps." + }, + "emptyVaultNudgeTitle": { + "message": "Import existing passwords" + }, + "emptyVaultNudgeBody": { + "message": "Use the importer to quickly transfer logins to Bitwarden without manually adding them." + }, + "emptyVaultNudgeButton": { + "message": "Import now" + }, + "hasItemsVaultNudgeTitle": { + "message": "Welcome to your vault!" + }, + "hasItemsVaultNudgeBody": { + "message": "Autofill items for the current page\nFavorite items for easy access\nSearch your vault for something else" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/kn/messages.json b/apps/browser/src/_locales/kn/messages.json index d12738ed231..408727f7ec8 100644 --- a/apps/browser/src/_locales/kn/messages.json +++ b/apps/browser/src/_locales/kn/messages.json @@ -5201,6 +5201,12 @@ "changeAtRiskPassword": { "message": "Change at-risk password" }, + "settingsVaultOptions": { + "message": "Vault options" + }, + "emptyVaultDescription": { + "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + }, "introCarouselLabel": { "message": "Welcome to Bitwarden" }, @@ -5227,5 +5233,50 @@ }, "secureDevicesBody": { "message": "Save unlimited passwords across unlimited devices with Bitwarden mobile, browser, and desktop apps." + }, + "emptyVaultNudgeTitle": { + "message": "Import existing passwords" + }, + "emptyVaultNudgeBody": { + "message": "Use the importer to quickly transfer logins to Bitwarden without manually adding them." + }, + "emptyVaultNudgeButton": { + "message": "Import now" + }, + "hasItemsVaultNudgeTitle": { + "message": "Welcome to your vault!" + }, + "hasItemsVaultNudgeBody": { + "message": "Autofill items for the current page\nFavorite items for easy access\nSearch your vault for something else" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/ko/messages.json b/apps/browser/src/_locales/ko/messages.json index 2d30255100e..01cb0fbf7c3 100644 --- a/apps/browser/src/_locales/ko/messages.json +++ b/apps/browser/src/_locales/ko/messages.json @@ -5201,6 +5201,12 @@ "changeAtRiskPassword": { "message": "Change at-risk password" }, + "settingsVaultOptions": { + "message": "Vault options" + }, + "emptyVaultDescription": { + "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + }, "introCarouselLabel": { "message": "Welcome to Bitwarden" }, @@ -5227,5 +5233,50 @@ }, "secureDevicesBody": { "message": "Save unlimited passwords across unlimited devices with Bitwarden mobile, browser, and desktop apps." + }, + "emptyVaultNudgeTitle": { + "message": "Import existing passwords" + }, + "emptyVaultNudgeBody": { + "message": "Use the importer to quickly transfer logins to Bitwarden without manually adding them." + }, + "emptyVaultNudgeButton": { + "message": "Import now" + }, + "hasItemsVaultNudgeTitle": { + "message": "Welcome to your vault!" + }, + "hasItemsVaultNudgeBody": { + "message": "Autofill items for the current page\nFavorite items for easy access\nSearch your vault for something else" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/lt/messages.json b/apps/browser/src/_locales/lt/messages.json index 465f0fdad38..51879334614 100644 --- a/apps/browser/src/_locales/lt/messages.json +++ b/apps/browser/src/_locales/lt/messages.json @@ -5201,6 +5201,12 @@ "changeAtRiskPassword": { "message": "Change at-risk password" }, + "settingsVaultOptions": { + "message": "Vault options" + }, + "emptyVaultDescription": { + "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + }, "introCarouselLabel": { "message": "Welcome to Bitwarden" }, @@ -5227,5 +5233,50 @@ }, "secureDevicesBody": { "message": "Save unlimited passwords across unlimited devices with Bitwarden mobile, browser, and desktop apps." + }, + "emptyVaultNudgeTitle": { + "message": "Import existing passwords" + }, + "emptyVaultNudgeBody": { + "message": "Use the importer to quickly transfer logins to Bitwarden without manually adding them." + }, + "emptyVaultNudgeButton": { + "message": "Import now" + }, + "hasItemsVaultNudgeTitle": { + "message": "Welcome to your vault!" + }, + "hasItemsVaultNudgeBody": { + "message": "Autofill items for the current page\nFavorite items for easy access\nSearch your vault for something else" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/lv/messages.json b/apps/browser/src/_locales/lv/messages.json index 2d73bacc5c9..6c39d9ff3e6 100644 --- a/apps/browser/src/_locales/lv/messages.json +++ b/apps/browser/src/_locales/lv/messages.json @@ -437,7 +437,7 @@ "message": "Sinhronizēt glabātavu" }, "lastSync": { - "message": "Pēdējā sinhronizācija:" + "message": "Pēdējā sinhronizēšana:" }, "passGen": { "message": "Paroļu veidotājs" @@ -947,10 +947,10 @@ "message": "Noskaties mūsu uzsākšanas pamācību, lai uzzinātu, kā iegūt vislielāko labumu no pārlūka paplašinājuma!" }, "syncingComplete": { - "message": "Sinhronizācija pabeigta" + "message": "Sinhronizēšana pabeigta" }, "syncingFailed": { - "message": "Sinhronizācija neizdevās" + "message": "Sinhronizēšana neizdevās" }, "passwordCopied": { "message": "Parole ievietota starpliktuvē" @@ -1543,7 +1543,7 @@ "message": "Jāievada e-pastā nosūtītais kods." }, "selfHostedEnvironment": { - "message": "Pašuzturēta vide" + "message": "Pašmitināta vide" }, "selfHostedBaseUrlHint": { "message": "Jānorāda sava pašizvietotā Bitward servera pamata URL. Piemērs: https://bitwarden.uznemums.lv" @@ -2370,7 +2370,7 @@ "message": "Netika atrastas atsvaidzināšanas pilnvaras vai API atslēgas. Lūgums mēģināt izrakstīties un atkal pieteikties." }, "desktopSyncVerificationTitle": { - "message": "Darbvirsmas sinhronizācijas apstiprinājums" + "message": "Darbvirsmas sinhronizēšanas apliecinājums" }, "desktopIntegrationVerificationText": { "message": "Lūgumus pārliecināties, ka darbvirsmas lietotne rāda šo atpazīšanas vārdkopu:" @@ -2996,7 +2996,7 @@ "message": "Nav atrasts neviens neatkārtojams identifikators" }, "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ izmanto vienoto pieteikšanos ar pašizvietotu atslēgu serveri. Tās dalībniekiem vairs nav nepieciešama galvenā parole, lai pieteiktos.", + "message": "$ORGANIZATION$ izmanto vienoto pieteikšanos ar pašmitinātu atslēgu serveri. Tās dalībniekiem vairs nav nepieciešama galvenā parole, lai pieteiktos.", "placeholders": { "organization": { "content": "$1", @@ -3315,7 +3315,7 @@ "message": "Servera versija" }, "selfHostedServer": { - "message": "pašizvietots" + "message": "pašmitināts" }, "thirdParty": { "message": "Trešās puses" @@ -5201,6 +5201,12 @@ "changeAtRiskPassword": { "message": "Mainīt riskam pakļautu paroli" }, + "settingsVaultOptions": { + "message": "Glabātavas iespējas" + }, + "emptyVaultDescription": { + "message": "Glabātava aizsargā vairāk kā tikai paroles. Drošā veidā glabā šeit pieteikšanās vienumus, identitātes, kartes un piezīmes!" + }, "introCarouselLabel": { "message": "Laipni lūdzam Bitwarden" }, @@ -5227,5 +5233,50 @@ }, "secureDevicesBody": { "message": "Neierobežotu paroļu skaitu var saglabāt neierobežotā ierīdžu daudzumā ar Bitwarden viedtālruņa, pārlūka un darbvirsmas lietotni." + }, + "emptyVaultNudgeTitle": { + "message": "Ievietot esošas paroles" + }, + "emptyVaultNudgeBody": { + "message": "Ievietotājs ir izmantojams, lai pieteikšanās vienumus ātri pārnest uz Bitwarden bez pašrocīgas to pievienošanas." + }, + "emptyVaultNudgeButton": { + "message": "Ievietot tagad" + }, + "hasItemsVaultNudgeTitle": { + "message": "Laipni lūdzam Tavā glabātavā!" + }, + "hasItemsVaultNudgeBody": { + "message": "Automātiskās aizpldes vienumu pašreizējai lapai\nIzlases vienumi vieglai piekļuvei\nPārējo var meklēt glabātavā" + }, + "newLoginNudgeTitle": { + "message": "Laika ietaupīšana ar automātisko aizpildi" + }, + "newLoginNudgeBody": { + "message": "Iekļauj tīmekļvietni, lai šis pieteikšanās vienums parādītos kā automātiskās aizpildes ieteikums!" + }, + "newCardNudgeTitle": { + "message": "Plūdena apmaksa tiešsaistē" + }, + "newCardNudgeBody": { + "message": "Ar kartēm ir viegli automātiski aizpildīt maksājumu veidlapu droši un rūpīgi." + }, + "newIdentityNudgeTitle": { + "message": "Kontu izveidošanas vienkāršošana" + }, + "newIdentityNudgeBody": { + "message": "Ar identitātēm var ātri automātiski aizpildīt garas reģistrēšanās vai saziņas veidlapas." + }, + "newNoteNudgeTitle": { + "message": "Turi savus jūtīgos datus drošībā" + }, + "newNoteNudgeBody": { + "message": "Piezīmēs var droši glabāt jūtīgus datus, piemēram, bankas vai apdrošināšanas informāciju." + }, + "newSshNudgeTitle": { + "message": "Izstrādātājiem draudzīga SSH piekļuve" + }, + "newSshNudgeBody": { + "message": "Glabā savas atslēgas un savienojies ar SSH aģentu ātrai, šifrētai autentificēšanai!" } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/ml/messages.json b/apps/browser/src/_locales/ml/messages.json index e5b2e89cee6..15d35d29c76 100644 --- a/apps/browser/src/_locales/ml/messages.json +++ b/apps/browser/src/_locales/ml/messages.json @@ -5201,6 +5201,12 @@ "changeAtRiskPassword": { "message": "Change at-risk password" }, + "settingsVaultOptions": { + "message": "Vault options" + }, + "emptyVaultDescription": { + "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + }, "introCarouselLabel": { "message": "Welcome to Bitwarden" }, @@ -5227,5 +5233,50 @@ }, "secureDevicesBody": { "message": "Save unlimited passwords across unlimited devices with Bitwarden mobile, browser, and desktop apps." + }, + "emptyVaultNudgeTitle": { + "message": "Import existing passwords" + }, + "emptyVaultNudgeBody": { + "message": "Use the importer to quickly transfer logins to Bitwarden without manually adding them." + }, + "emptyVaultNudgeButton": { + "message": "Import now" + }, + "hasItemsVaultNudgeTitle": { + "message": "Welcome to your vault!" + }, + "hasItemsVaultNudgeBody": { + "message": "Autofill items for the current page\nFavorite items for easy access\nSearch your vault for something else" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/mr/messages.json b/apps/browser/src/_locales/mr/messages.json index 221015a7b42..8dee37bfcb8 100644 --- a/apps/browser/src/_locales/mr/messages.json +++ b/apps/browser/src/_locales/mr/messages.json @@ -5201,6 +5201,12 @@ "changeAtRiskPassword": { "message": "Change at-risk password" }, + "settingsVaultOptions": { + "message": "Vault options" + }, + "emptyVaultDescription": { + "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + }, "introCarouselLabel": { "message": "Welcome to Bitwarden" }, @@ -5227,5 +5233,50 @@ }, "secureDevicesBody": { "message": "Save unlimited passwords across unlimited devices with Bitwarden mobile, browser, and desktop apps." + }, + "emptyVaultNudgeTitle": { + "message": "Import existing passwords" + }, + "emptyVaultNudgeBody": { + "message": "Use the importer to quickly transfer logins to Bitwarden without manually adding them." + }, + "emptyVaultNudgeButton": { + "message": "Import now" + }, + "hasItemsVaultNudgeTitle": { + "message": "Welcome to your vault!" + }, + "hasItemsVaultNudgeBody": { + "message": "Autofill items for the current page\nFavorite items for easy access\nSearch your vault for something else" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/my/messages.json b/apps/browser/src/_locales/my/messages.json index 4f83b07506b..99a09c75d41 100644 --- a/apps/browser/src/_locales/my/messages.json +++ b/apps/browser/src/_locales/my/messages.json @@ -5201,6 +5201,12 @@ "changeAtRiskPassword": { "message": "Change at-risk password" }, + "settingsVaultOptions": { + "message": "Vault options" + }, + "emptyVaultDescription": { + "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + }, "introCarouselLabel": { "message": "Welcome to Bitwarden" }, @@ -5227,5 +5233,50 @@ }, "secureDevicesBody": { "message": "Save unlimited passwords across unlimited devices with Bitwarden mobile, browser, and desktop apps." + }, + "emptyVaultNudgeTitle": { + "message": "Import existing passwords" + }, + "emptyVaultNudgeBody": { + "message": "Use the importer to quickly transfer logins to Bitwarden without manually adding them." + }, + "emptyVaultNudgeButton": { + "message": "Import now" + }, + "hasItemsVaultNudgeTitle": { + "message": "Welcome to your vault!" + }, + "hasItemsVaultNudgeBody": { + "message": "Autofill items for the current page\nFavorite items for easy access\nSearch your vault for something else" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/nb/messages.json b/apps/browser/src/_locales/nb/messages.json index 81c7682680c..f3d87aaa067 100644 --- a/apps/browser/src/_locales/nb/messages.json +++ b/apps/browser/src/_locales/nb/messages.json @@ -1185,7 +1185,7 @@ "message": "Ja, oppdater nå" }, "notificationUnlockDesc": { - "message": "Unlock your Bitwarden vault to complete the autofill request." + "message": "Lås opp Bitwarden-hvelvet ditt for å utføre auto-utfyllingen." }, "notificationUnlock": { "message": "Lås opp" @@ -5201,6 +5201,12 @@ "changeAtRiskPassword": { "message": "Change at-risk password" }, + "settingsVaultOptions": { + "message": "Vault options" + }, + "emptyVaultDescription": { + "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + }, "introCarouselLabel": { "message": "Welcome to Bitwarden" }, @@ -5227,5 +5233,50 @@ }, "secureDevicesBody": { "message": "Save unlimited passwords across unlimited devices with Bitwarden mobile, browser, and desktop apps." + }, + "emptyVaultNudgeTitle": { + "message": "Import existing passwords" + }, + "emptyVaultNudgeBody": { + "message": "Use the importer to quickly transfer logins to Bitwarden without manually adding them." + }, + "emptyVaultNudgeButton": { + "message": "Import now" + }, + "hasItemsVaultNudgeTitle": { + "message": "Welcome to your vault!" + }, + "hasItemsVaultNudgeBody": { + "message": "Autofill items for the current page\nFavorite items for easy access\nSearch your vault for something else" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/ne/messages.json b/apps/browser/src/_locales/ne/messages.json index 4f83b07506b..99a09c75d41 100644 --- a/apps/browser/src/_locales/ne/messages.json +++ b/apps/browser/src/_locales/ne/messages.json @@ -5201,6 +5201,12 @@ "changeAtRiskPassword": { "message": "Change at-risk password" }, + "settingsVaultOptions": { + "message": "Vault options" + }, + "emptyVaultDescription": { + "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + }, "introCarouselLabel": { "message": "Welcome to Bitwarden" }, @@ -5227,5 +5233,50 @@ }, "secureDevicesBody": { "message": "Save unlimited passwords across unlimited devices with Bitwarden mobile, browser, and desktop apps." + }, + "emptyVaultNudgeTitle": { + "message": "Import existing passwords" + }, + "emptyVaultNudgeBody": { + "message": "Use the importer to quickly transfer logins to Bitwarden without manually adding them." + }, + "emptyVaultNudgeButton": { + "message": "Import now" + }, + "hasItemsVaultNudgeTitle": { + "message": "Welcome to your vault!" + }, + "hasItemsVaultNudgeBody": { + "message": "Autofill items for the current page\nFavorite items for easy access\nSearch your vault for something else" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/nl/messages.json b/apps/browser/src/_locales/nl/messages.json index afe7821e096..2934c5da3fd 100644 --- a/apps/browser/src/_locales/nl/messages.json +++ b/apps/browser/src/_locales/nl/messages.json @@ -5201,6 +5201,12 @@ "changeAtRiskPassword": { "message": "Risicovol wachtwoord wijzigen" }, + "settingsVaultOptions": { + "message": "Kluis-instellingen" + }, + "emptyVaultDescription": { + "message": "De kluis beschermt meer dan alleen je wachtwoorden. Sla hier beveiligde inloggegevens, ID's, kaarten en notities op." + }, "introCarouselLabel": { "message": "Welkom bij Bitwarden" }, @@ -5227,5 +5233,50 @@ }, "secureDevicesBody": { "message": "Onbeperkt wachtwoorden opslaan op alle apparaten met Bitwarden-apps voor mobiel, browser en desktop." + }, + "emptyVaultNudgeTitle": { + "message": "Bestaande wachtwoorden importeren" + }, + "emptyVaultNudgeBody": { + "message": "Gebruik de importer om snel logins naar Bitwarden over te dragen zonder ze handmatig toe te voegen." + }, + "emptyVaultNudgeButton": { + "message": "Nu importeren" + }, + "hasItemsVaultNudgeTitle": { + "message": "Welkom in je kluis!" + }, + "hasItemsVaultNudgeBody": { + "message": "Autofill items voor de huidige pagina\nFavoriete items voor eenvoudige toegang\nDoorzoek je kluis naar iets anders" + }, + "newLoginNudgeTitle": { + "message": "Tijd besparen met automatisch aanvullen" + }, + "newLoginNudgeBody": { + "message": "Voeg een website toe zodat deze login wordt weergegeven als een automatische invulsuggestie." + }, + "newCardNudgeTitle": { + "message": "Naadloos online afrekenen" + }, + "newCardNudgeBody": { + "message": "Met kaarten gemakkelijk, veilig en accuraat automatisch invullen van betaalformulieren." + }, + "newIdentityNudgeTitle": { + "message": "Vereenvoudig het aanmaken van accounts" + }, + "newIdentityNudgeBody": { + "message": "Met identiteiten vul je lange registratie- of contactformulieren snel automatisch in." + }, + "newNoteNudgeTitle": { + "message": "Houd je gevoelige gegevens veilig" + }, + "newNoteNudgeBody": { + "message": "Met notities veilig opslaan van gevoelige gegevens zoals bank- of verzekeringsgegevens." + }, + "newSshNudgeTitle": { + "message": "Ontwikkelaars-vriendelijke SSH-toegang" + }, + "newSshNudgeBody": { + "message": "Sla je sleutels op en verbind met de SSH-agent voor snelle, versleutelde authenticatie." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/nn/messages.json b/apps/browser/src/_locales/nn/messages.json index 4f83b07506b..99a09c75d41 100644 --- a/apps/browser/src/_locales/nn/messages.json +++ b/apps/browser/src/_locales/nn/messages.json @@ -5201,6 +5201,12 @@ "changeAtRiskPassword": { "message": "Change at-risk password" }, + "settingsVaultOptions": { + "message": "Vault options" + }, + "emptyVaultDescription": { + "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + }, "introCarouselLabel": { "message": "Welcome to Bitwarden" }, @@ -5227,5 +5233,50 @@ }, "secureDevicesBody": { "message": "Save unlimited passwords across unlimited devices with Bitwarden mobile, browser, and desktop apps." + }, + "emptyVaultNudgeTitle": { + "message": "Import existing passwords" + }, + "emptyVaultNudgeBody": { + "message": "Use the importer to quickly transfer logins to Bitwarden without manually adding them." + }, + "emptyVaultNudgeButton": { + "message": "Import now" + }, + "hasItemsVaultNudgeTitle": { + "message": "Welcome to your vault!" + }, + "hasItemsVaultNudgeBody": { + "message": "Autofill items for the current page\nFavorite items for easy access\nSearch your vault for something else" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/or/messages.json b/apps/browser/src/_locales/or/messages.json index 4f83b07506b..99a09c75d41 100644 --- a/apps/browser/src/_locales/or/messages.json +++ b/apps/browser/src/_locales/or/messages.json @@ -5201,6 +5201,12 @@ "changeAtRiskPassword": { "message": "Change at-risk password" }, + "settingsVaultOptions": { + "message": "Vault options" + }, + "emptyVaultDescription": { + "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + }, "introCarouselLabel": { "message": "Welcome to Bitwarden" }, @@ -5227,5 +5233,50 @@ }, "secureDevicesBody": { "message": "Save unlimited passwords across unlimited devices with Bitwarden mobile, browser, and desktop apps." + }, + "emptyVaultNudgeTitle": { + "message": "Import existing passwords" + }, + "emptyVaultNudgeBody": { + "message": "Use the importer to quickly transfer logins to Bitwarden without manually adding them." + }, + "emptyVaultNudgeButton": { + "message": "Import now" + }, + "hasItemsVaultNudgeTitle": { + "message": "Welcome to your vault!" + }, + "hasItemsVaultNudgeBody": { + "message": "Autofill items for the current page\nFavorite items for easy access\nSearch your vault for something else" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/pl/messages.json b/apps/browser/src/_locales/pl/messages.json index 1cc26eb5ef1..042cafb0300 100644 --- a/apps/browser/src/_locales/pl/messages.json +++ b/apps/browser/src/_locales/pl/messages.json @@ -5201,6 +5201,12 @@ "changeAtRiskPassword": { "message": "Zmień hasło zagrożone" }, + "settingsVaultOptions": { + "message": "Ustawienia Sejfu" + }, + "emptyVaultDescription": { + "message": "Sejf chroni nie tylko Twoje hasła. Przechowuj tutaj bezpiecznie loginy, identyfikatory, karty i notatki." + }, "introCarouselLabel": { "message": "Witaj w Bitwarden" }, @@ -5227,5 +5233,50 @@ }, "secureDevicesBody": { "message": "Zapisuj nieograniczoną liczbę haseł na nieograniczonej liczbie urządzeń dzięki aplikacjom Bitwarden na urządzenia mobilne, przeglądarki i komputery stacjonarne." + }, + "emptyVaultNudgeTitle": { + "message": "Importuj istniejące hasła" + }, + "emptyVaultNudgeBody": { + "message": "Użyj importera, aby szybko przenieść loginy do Bitwarden bez ręcznego dodawania ich." + }, + "emptyVaultNudgeButton": { + "message": "Importuj teraz" + }, + "hasItemsVaultNudgeTitle": { + "message": "Witaj w Twoim Sejfie!" + }, + "hasItemsVaultNudgeBody": { + "message": "Autouzupełnianie elementów dla bieżącej strony\nUlubione elementy dla łatwego dostępu\nPrzeszukaj swój sejf w poszukiwaniu czegoś innego" + }, + "newLoginNudgeTitle": { + "message": "Oszczędzaj czas dzięki autouzupełnianiu" + }, + "newLoginNudgeBody": { + "message": "Dołącz stronę internetową, aby ten login pojawił się jako sugestia autouzupełniania." + }, + "newCardNudgeTitle": { + "message": "Bezproblemowe zamówienia online" + }, + "newCardNudgeBody": { + "message": "Z kartami łatwe autouzupełnianie formularzy płatności w sposób bezpieczny i dokładny." + }, + "newIdentityNudgeTitle": { + "message": "Uprość tworzenie kont" + }, + "newIdentityNudgeBody": { + "message": "Z tożsamościami, szybko autouzupełnij długie formularze rejestracyjne lub kontaktowe." + }, + "newNoteNudgeTitle": { + "message": "Zachowaj bezpieczeństwo wrażliwych danych" + }, + "newNoteNudgeBody": { + "message": "Z notatkami bezpiecznie przechowuj dane szczególnie chronione, takie jak dane bankowe lub ubezpieczeniowe." + }, + "newSshNudgeTitle": { + "message": "Przyjazny dla deweloperów dostęp SSH" + }, + "newSshNudgeBody": { + "message": "Przechowuj swoje klucze i połącz się z agentem SSH dla szybkiego, szyfrowanego uwierzytelniania." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/pt_BR/messages.json b/apps/browser/src/_locales/pt_BR/messages.json index cd516ec83a6..c4fb561b5be 100644 --- a/apps/browser/src/_locales/pt_BR/messages.json +++ b/apps/browser/src/_locales/pt_BR/messages.json @@ -5201,6 +5201,12 @@ "changeAtRiskPassword": { "message": "Alterar senhas vulneráveis" }, + "settingsVaultOptions": { + "message": "Vault options" + }, + "emptyVaultDescription": { + "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + }, "introCarouselLabel": { "message": "Bem-vindo(a) ao Bitwarden" }, @@ -5227,5 +5233,50 @@ }, "secureDevicesBody": { "message": "Guarde quantas senhas quiser e acesse de qualquer lugar com o Bitwarden. No seu celular, navegador e computador." + }, + "emptyVaultNudgeTitle": { + "message": "Import existing passwords" + }, + "emptyVaultNudgeBody": { + "message": "Use the importer to quickly transfer logins to Bitwarden without manually adding them." + }, + "emptyVaultNudgeButton": { + "message": "Import now" + }, + "hasItemsVaultNudgeTitle": { + "message": "Welcome to your vault!" + }, + "hasItemsVaultNudgeBody": { + "message": "Autofill items for the current page\nFavorite items for easy access\nSearch your vault for something else" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/pt_PT/messages.json b/apps/browser/src/_locales/pt_PT/messages.json index 16746dac172..0af5cb2d13f 100644 --- a/apps/browser/src/_locales/pt_PT/messages.json +++ b/apps/browser/src/_locales/pt_PT/messages.json @@ -5201,6 +5201,12 @@ "changeAtRiskPassword": { "message": "Alterar palavra-passe em risco" }, + "settingsVaultOptions": { + "message": "Opções do cofre" + }, + "emptyVaultDescription": { + "message": "O cofre protege mais do que apenas as suas palavras-passe. Guarde aqui credenciais, IDs, cartões e notas de forma segura." + }, "introCarouselLabel": { "message": "Bem-vindo ao Bitwarden" }, @@ -5227,5 +5233,50 @@ }, "secureDevicesBody": { "message": "Guarde palavras-passe ilimitadas em dispositivos ilimitados com as apps Bitwarden para telemóvel, navegador e computador." + }, + "emptyVaultNudgeTitle": { + "message": "Importar palavras-passe existentes" + }, + "emptyVaultNudgeBody": { + "message": "Utilize o importador para transferir rapidamente as credenciais para o Bitwarden sem as adicionar manualmente." + }, + "emptyVaultNudgeButton": { + "message": "Importar agora" + }, + "hasItemsVaultNudgeTitle": { + "message": "Bem-vindo ao seu cofre!" + }, + "hasItemsVaultNudgeBody": { + "message": "Preenchimento automático de itens para a página atual\nItens favoritos para um acesso fácil\nProcurar outra coisa no seu cofre" + }, + "newLoginNudgeTitle": { + "message": "Poupe tempo com o preenchimento automático" + }, + "newLoginNudgeBody": { + "message": "Inclua um site para que esta credencial apareça como uma sugestão de preenchimento automático." + }, + "newCardNudgeTitle": { + "message": "Pagamentos online sem problemas" + }, + "newCardNudgeBody": { + "message": "Com os cartões, preencha facilmente formulários de pagamento de forma segura e exata." + }, + "newIdentityNudgeTitle": { + "message": "Simplifique a criação de contas" + }, + "newIdentityNudgeBody": { + "message": "Com as identidades, preencha rapidamente formulários de registo ou de contacto longos." + }, + "newNoteNudgeTitle": { + "message": "Mantenha os seus dados sensíveis seguros" + }, + "newNoteNudgeBody": { + "message": "Com as notas, armazene de forma segura dados sensíveis, como dados bancários ou de seguros." + }, + "newSshNudgeTitle": { + "message": "Acesso SSH de fácil utilização pelos programadores" + }, + "newSshNudgeBody": { + "message": "Guarde as suas chaves e ligue-se ao agente SSH para uma autenticação rápida e encriptada." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/ro/messages.json b/apps/browser/src/_locales/ro/messages.json index ba3cbcf9469..bf08e0969b2 100644 --- a/apps/browser/src/_locales/ro/messages.json +++ b/apps/browser/src/_locales/ro/messages.json @@ -5201,6 +5201,12 @@ "changeAtRiskPassword": { "message": "Change at-risk password" }, + "settingsVaultOptions": { + "message": "Vault options" + }, + "emptyVaultDescription": { + "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + }, "introCarouselLabel": { "message": "Welcome to Bitwarden" }, @@ -5227,5 +5233,50 @@ }, "secureDevicesBody": { "message": "Save unlimited passwords across unlimited devices with Bitwarden mobile, browser, and desktop apps." + }, + "emptyVaultNudgeTitle": { + "message": "Import existing passwords" + }, + "emptyVaultNudgeBody": { + "message": "Use the importer to quickly transfer logins to Bitwarden without manually adding them." + }, + "emptyVaultNudgeButton": { + "message": "Import now" + }, + "hasItemsVaultNudgeTitle": { + "message": "Welcome to your vault!" + }, + "hasItemsVaultNudgeBody": { + "message": "Autofill items for the current page\nFavorite items for easy access\nSearch your vault for something else" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/ru/messages.json b/apps/browser/src/_locales/ru/messages.json index cd146190373..f6343f6d2bb 100644 --- a/apps/browser/src/_locales/ru/messages.json +++ b/apps/browser/src/_locales/ru/messages.json @@ -5201,6 +5201,12 @@ "changeAtRiskPassword": { "message": "Изменить пароль, подверженный риску" }, + "settingsVaultOptions": { + "message": "Настройки хранилища" + }, + "emptyVaultDescription": { + "message": "Хранилище защищает не только ваши пароли. Логины, идентификаторы, карты и заметки в нем надежно защищены." + }, "introCarouselLabel": { "message": "Добро пожаловать в Bitwarden" }, @@ -5227,5 +5233,50 @@ }, "secureDevicesBody": { "message": "Сохраняйте неограниченное количество паролей на неограниченном количестве устройств с помощью мобильных, браузерных и десктопных приложений Bitwarden." + }, + "emptyVaultNudgeTitle": { + "message": "Импорт существующих паролей" + }, + "emptyVaultNudgeBody": { + "message": "Используйте импортер, чтобы быстро перенести логины в Bitwarden без их ручного добавления." + }, + "emptyVaultNudgeButton": { + "message": "Импортировать сейчас" + }, + "hasItemsVaultNudgeTitle": { + "message": "Добро пожаловать в ваше хранилище!" + }, + "hasItemsVaultNudgeBody": { + "message": "Автозаполнение элементов для текущей страницы\nИзбранные элементы для легкого доступа\nПоиск в хранилище для чего-либо еще" + }, + "newLoginNudgeTitle": { + "message": "Экономьте время с помощью автозаполнения" + }, + "newLoginNudgeBody": { + "message": "Включите сайт, чтобы этот логин отображался в качестве предложения для автозаполнения." + }, + "newCardNudgeTitle": { + "message": "Оформление заказа через интернет" + }, + "newCardNudgeBody": { + "message": "С помощью карт можно легко и безопасно автоматически заполнять формы оплаты." + }, + "newIdentityNudgeTitle": { + "message": "Упрощение создания аккаунтов" + }, + "newIdentityNudgeBody": { + "message": "С помощью личностей можно быстро заполнять длинные регистрационные или контактные формы." + }, + "newNoteNudgeTitle": { + "message": "Храните ваши конфиденциальные данные в безопасности" + }, + "newNoteNudgeBody": { + "message": "С помощью заметок можно надежно хранить конфиденциальные данные, например, банковские или страховые реквизиты." + }, + "newSshNudgeTitle": { + "message": "Удобный для разработчиков SSH-доступ" + }, + "newSshNudgeBody": { + "message": "Храните свои ключи и подключайтесь с помощью агента SSH для быстрой и зашифрованной аутентификации." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/si/messages.json b/apps/browser/src/_locales/si/messages.json index 166b5490003..a5f7e772505 100644 --- a/apps/browser/src/_locales/si/messages.json +++ b/apps/browser/src/_locales/si/messages.json @@ -5201,6 +5201,12 @@ "changeAtRiskPassword": { "message": "Change at-risk password" }, + "settingsVaultOptions": { + "message": "Vault options" + }, + "emptyVaultDescription": { + "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + }, "introCarouselLabel": { "message": "Welcome to Bitwarden" }, @@ -5227,5 +5233,50 @@ }, "secureDevicesBody": { "message": "Save unlimited passwords across unlimited devices with Bitwarden mobile, browser, and desktop apps." + }, + "emptyVaultNudgeTitle": { + "message": "Import existing passwords" + }, + "emptyVaultNudgeBody": { + "message": "Use the importer to quickly transfer logins to Bitwarden without manually adding them." + }, + "emptyVaultNudgeButton": { + "message": "Import now" + }, + "hasItemsVaultNudgeTitle": { + "message": "Welcome to your vault!" + }, + "hasItemsVaultNudgeBody": { + "message": "Autofill items for the current page\nFavorite items for easy access\nSearch your vault for something else" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/sk/messages.json b/apps/browser/src/_locales/sk/messages.json index 70ff5bbfc27..5f65633e44c 100644 --- a/apps/browser/src/_locales/sk/messages.json +++ b/apps/browser/src/_locales/sk/messages.json @@ -5201,6 +5201,12 @@ "changeAtRiskPassword": { "message": "Zmeniť rizikové heslá" }, + "settingsVaultOptions": { + "message": "Možnosti trezoru" + }, + "emptyVaultDescription": { + "message": "Trezor chráni viac ako len heslá. Môžete tu bezpečne ukladať prihlasovacie údaje, identifikačné údaje, karty a poznámky." + }, "introCarouselLabel": { "message": "Vitajte v Bitwardene" }, @@ -5227,5 +5233,50 @@ }, "secureDevicesBody": { "message": "Ukladajte neobmedzený počet hesiel na neobmedzenom počte zariadení pomocou mobilných aplikácií, prehliadačov a desktopových aplikácií Bitwardenu." + }, + "emptyVaultNudgeTitle": { + "message": "Import existujúcich hesiel" + }, + "emptyVaultNudgeBody": { + "message": "Pomocou nástroja na import môžete rýchlo preniesť prihlasovacie údaje do Bitwardenu bez ručného pridávania." + }, + "emptyVaultNudgeButton": { + "message": "Importovať teraz" + }, + "hasItemsVaultNudgeTitle": { + "message": "Vitajte vo svojom trezore!" + }, + "hasItemsVaultNudgeBody": { + "message": "Automatické vypĺňanie položiek pre aktuálnu stránku\nObľúbené položky pre ľahký prístup\nVyhľadajte v trezore niečo iné" + }, + "newLoginNudgeTitle": { + "message": "Ušetrite čas s automatickým vypĺňaním" + }, + "newLoginNudgeBody": { + "message": "Zadajte webovú stránku, aby sa tieto prihlasovacie údaje zobrazili ako návrh na automatické vyplnenie." + }, + "newCardNudgeTitle": { + "message": "Bezproblémová online registrácia" + }, + "newCardNudgeBody": { + "message": "S kartami môžete jednoducho, bezpečne a presne automaticky vypĺňať platobné formuláre." + }, + "newIdentityNudgeTitle": { + "message": "Zjednodušenie vytvárania účtov" + }, + "newIdentityNudgeBody": { + "message": "Pomocou identít môžete rýchlo automaticky vypĺňať dlhé registračné alebo kontaktné formuláre." + }, + "newNoteNudgeTitle": { + "message": "Udržujte svoje citlivé údaje v bezpečí" + }, + "newNoteNudgeBody": { + "message": "Pomocou poznámok môžete bezpečne ukladať citlivé údaje, napríklad bankové údaje alebo údaje o poistení." + }, + "newSshNudgeTitle": { + "message": "Prístup SSH vhodný pre vývojárov" + }, + "newSshNudgeBody": { + "message": "Uložte si kľúče a pripojte sa pomocou agenta SSH na rýchle šifrované overovanie." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/sl/messages.json b/apps/browser/src/_locales/sl/messages.json index b0c815f2089..c7585878d8a 100644 --- a/apps/browser/src/_locales/sl/messages.json +++ b/apps/browser/src/_locales/sl/messages.json @@ -5201,6 +5201,12 @@ "changeAtRiskPassword": { "message": "Change at-risk password" }, + "settingsVaultOptions": { + "message": "Vault options" + }, + "emptyVaultDescription": { + "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + }, "introCarouselLabel": { "message": "Welcome to Bitwarden" }, @@ -5227,5 +5233,50 @@ }, "secureDevicesBody": { "message": "Save unlimited passwords across unlimited devices with Bitwarden mobile, browser, and desktop apps." + }, + "emptyVaultNudgeTitle": { + "message": "Import existing passwords" + }, + "emptyVaultNudgeBody": { + "message": "Use the importer to quickly transfer logins to Bitwarden without manually adding them." + }, + "emptyVaultNudgeButton": { + "message": "Import now" + }, + "hasItemsVaultNudgeTitle": { + "message": "Welcome to your vault!" + }, + "hasItemsVaultNudgeBody": { + "message": "Autofill items for the current page\nFavorite items for easy access\nSearch your vault for something else" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/sr/messages.json b/apps/browser/src/_locales/sr/messages.json index bc1304270f8..a5b18b0da21 100644 --- a/apps/browser/src/_locales/sr/messages.json +++ b/apps/browser/src/_locales/sr/messages.json @@ -887,7 +887,7 @@ "message": "Следите наведене кораке да бисте завршили пријављивање." }, "followTheStepsBelowToFinishLoggingInWithSecurityKey": { - "message": "Follow the steps below to finish logging in with your security key." + "message": "Следите наведене кораке да бисте завршили пријаву са својим безбедносним кључем." }, "restartRegistration": { "message": "Поново покрените регистрацију" @@ -1063,7 +1063,7 @@ "message": "Сачувај" }, "notificationViewAria": { - "message": "View $ITEMNAME$, opens in new window", + "message": "Преглед $ITEMNAME$, отвара се у новом прозору", "placeholders": { "itemName": { "content": "$1" @@ -1072,14 +1072,14 @@ "description": "Aria label for the view button in notification bar confirmation message" }, "notificationEditTooltip": { - "message": "Edit before saving", + "message": "Уреди пре сачувавања", "description": "Tooltip and Aria label for edit button on cipher item" }, "newNotification": { - "message": "New notification" + "message": "Ново обавештење" }, "labelWithNotification": { - "message": "$LABEL$: New notification", + "message": "$LABEL$: Ново обавештење", "description": "Label for the notification with a new login suggestion.", "placeholders": { "label": { @@ -1089,7 +1089,7 @@ } }, "loginSaveConfirmation": { - "message": "$ITEMNAME$ saved to Bitwarden.", + "message": "$ITEMNAME$ сачувано и Bitwarden.", "placeholders": { "itemName": { "content": "$1" @@ -1098,7 +1098,7 @@ "description": "Shown to user after item is saved." }, "loginUpdatedConfirmation": { - "message": "$ITEMNAME$ updated in Bitwarden.", + "message": "$ITEMNAME$ ажурирано у Bitwarden.", "placeholders": { "itemName": { "content": "$1" @@ -1115,11 +1115,11 @@ "description": "Button text for updating an existing login entry." }, "saveLogin": { - "message": "Save login", + "message": "Сачувати пријаву", "description": "Prompt asking the user if they want to save their login details." }, "updateLogin": { - "message": "Update existing login", + "message": "Ажурирати постојећу пријаву", "description": "Prompt asking the user if they want to update an existing login entry." }, "loginSaveSuccess": { @@ -5201,6 +5201,12 @@ "changeAtRiskPassword": { "message": "Променити ризичну лозинку" }, + "settingsVaultOptions": { + "message": "Опције сефа" + }, + "emptyVaultDescription": { + "message": "Сеф штити више од само ваших лозинки. Овде безбедно чувајте безбедне пријаве, личне карте, картице и белешке." + }, "introCarouselLabel": { "message": "Добродошли у Bitwarden" }, @@ -5227,5 +5233,50 @@ }, "secureDevicesBody": { "message": "Сачувајте неограничене лозинке на неограниченим уређајима помоћу Bitwarden мобилних апликација, претраживача и десктоп апликација." + }, + "emptyVaultNudgeTitle": { + "message": "Увоз постојеће лозинке" + }, + "emptyVaultNudgeBody": { + "message": "Користите увозник да брзо преносите пријаве у Bitwarden без да их ручно додате." + }, + "emptyVaultNudgeButton": { + "message": "Увези сада" + }, + "hasItemsVaultNudgeTitle": { + "message": "Добродошли у ваш сеф!" + }, + "hasItemsVaultNudgeBody": { + "message": "Ауто-пуњење предмета за тренутну страницу\nОмиљени предмети за лак приступ\nПретражите сеф за нешто друго" + }, + "newLoginNudgeTitle": { + "message": "Уштедите време са ауто-пуњењем" + }, + "newLoginNudgeBody": { + "message": "Укључите веб страницу тако да се ова пријава појављује као предлог за ауто-пуњење." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "Са картицама, лако и сигурносно попуните формуларе за плаћање." + }, + "newIdentityNudgeTitle": { + "message": "Поједноставите креирање налога" + }, + "newIdentityNudgeBody": { + "message": "Са идентитетима, брзо попуните регистрације или контактних образаци." + }, + "newNoteNudgeTitle": { + "message": "Држите на сигурном своје осетљиве податке" + }, + "newNoteNudgeBody": { + "message": "Са белешкама, сигурно чувајте осетљиве податке попут банкарског или подаци о осигурању." + }, + "newSshNudgeTitle": { + "message": "Лак SSH приступ" + }, + "newSshNudgeBody": { + "message": "Чувајте кључеве и повежите се са SSH агент за брзу, шифровану аутентификацију." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/sv/messages.json b/apps/browser/src/_locales/sv/messages.json index edd000c97cf..27443c64140 100644 --- a/apps/browser/src/_locales/sv/messages.json +++ b/apps/browser/src/_locales/sv/messages.json @@ -5201,6 +5201,12 @@ "changeAtRiskPassword": { "message": "Change at-risk password" }, + "settingsVaultOptions": { + "message": "Vault options" + }, + "emptyVaultDescription": { + "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + }, "introCarouselLabel": { "message": "Welcome to Bitwarden" }, @@ -5227,5 +5233,50 @@ }, "secureDevicesBody": { "message": "Save unlimited passwords across unlimited devices with Bitwarden mobile, browser, and desktop apps." + }, + "emptyVaultNudgeTitle": { + "message": "Import existing passwords" + }, + "emptyVaultNudgeBody": { + "message": "Use the importer to quickly transfer logins to Bitwarden without manually adding them." + }, + "emptyVaultNudgeButton": { + "message": "Import now" + }, + "hasItemsVaultNudgeTitle": { + "message": "Welcome to your vault!" + }, + "hasItemsVaultNudgeBody": { + "message": "Autofill items for the current page\nFavorite items for easy access\nSearch your vault for something else" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/te/messages.json b/apps/browser/src/_locales/te/messages.json index 4f83b07506b..99a09c75d41 100644 --- a/apps/browser/src/_locales/te/messages.json +++ b/apps/browser/src/_locales/te/messages.json @@ -5201,6 +5201,12 @@ "changeAtRiskPassword": { "message": "Change at-risk password" }, + "settingsVaultOptions": { + "message": "Vault options" + }, + "emptyVaultDescription": { + "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + }, "introCarouselLabel": { "message": "Welcome to Bitwarden" }, @@ -5227,5 +5233,50 @@ }, "secureDevicesBody": { "message": "Save unlimited passwords across unlimited devices with Bitwarden mobile, browser, and desktop apps." + }, + "emptyVaultNudgeTitle": { + "message": "Import existing passwords" + }, + "emptyVaultNudgeBody": { + "message": "Use the importer to quickly transfer logins to Bitwarden without manually adding them." + }, + "emptyVaultNudgeButton": { + "message": "Import now" + }, + "hasItemsVaultNudgeTitle": { + "message": "Welcome to your vault!" + }, + "hasItemsVaultNudgeBody": { + "message": "Autofill items for the current page\nFavorite items for easy access\nSearch your vault for something else" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/th/messages.json b/apps/browser/src/_locales/th/messages.json index f9245cc7186..f6c8a9f0584 100644 --- a/apps/browser/src/_locales/th/messages.json +++ b/apps/browser/src/_locales/th/messages.json @@ -5201,6 +5201,12 @@ "changeAtRiskPassword": { "message": "Change at-risk password" }, + "settingsVaultOptions": { + "message": "Vault options" + }, + "emptyVaultDescription": { + "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + }, "introCarouselLabel": { "message": "Welcome to Bitwarden" }, @@ -5227,5 +5233,50 @@ }, "secureDevicesBody": { "message": "Save unlimited passwords across unlimited devices with Bitwarden mobile, browser, and desktop apps." + }, + "emptyVaultNudgeTitle": { + "message": "Import existing passwords" + }, + "emptyVaultNudgeBody": { + "message": "Use the importer to quickly transfer logins to Bitwarden without manually adding them." + }, + "emptyVaultNudgeButton": { + "message": "Import now" + }, + "hasItemsVaultNudgeTitle": { + "message": "Welcome to your vault!" + }, + "hasItemsVaultNudgeBody": { + "message": "Autofill items for the current page\nFavorite items for easy access\nSearch your vault for something else" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/tr/messages.json b/apps/browser/src/_locales/tr/messages.json index 57707940772..052941f2281 100644 --- a/apps/browser/src/_locales/tr/messages.json +++ b/apps/browser/src/_locales/tr/messages.json @@ -5201,6 +5201,12 @@ "changeAtRiskPassword": { "message": "Change at-risk password" }, + "settingsVaultOptions": { + "message": "Kasa seçenekleri" + }, + "emptyVaultDescription": { + "message": "Kasanız sadece parolalarınız için değil. Hesaplarınızı, kimliklerinizi, kredi kartlarınızı ve notlarınızı da güvenle burada depolayabilirsiniz." + }, "introCarouselLabel": { "message": "Bitwarden’a hoş geldiniz" }, @@ -5227,5 +5233,50 @@ }, "secureDevicesBody": { "message": "Bitwarden mobil, tarayıcı ve masaüstü uygulamalarıyla istediğiniz kadar cihaza istediğiniz kadar parola kaydedebilirsiniz." + }, + "emptyVaultNudgeTitle": { + "message": "Mevcut parolaları içe aktar" + }, + "emptyVaultNudgeBody": { + "message": "Use the importer to quickly transfer logins to Bitwarden without manually adding them." + }, + "emptyVaultNudgeButton": { + "message": "Şimdi içe aktar" + }, + "hasItemsVaultNudgeTitle": { + "message": "Kasanıza hoş geldiniz!" + }, + "hasItemsVaultNudgeBody": { + "message": "Autofill items for the current page\nFavorite items for easy access\nSearch your vault for something else" + }, + "newLoginNudgeTitle": { + "message": "Otomatik doldurmayla zaman kazanın" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Kesintisiz çevrimiçi alışveriş" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Hesap oluşturmak artık daha basit" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Hassas verilerinizi güvende tutun" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/uk/messages.json b/apps/browser/src/_locales/uk/messages.json index aebdce5b8f0..7b62690d03c 100644 --- a/apps/browser/src/_locales/uk/messages.json +++ b/apps/browser/src/_locales/uk/messages.json @@ -5201,6 +5201,12 @@ "changeAtRiskPassword": { "message": "Змінити ризикований пароль" }, + "settingsVaultOptions": { + "message": "Vault options" + }, + "emptyVaultDescription": { + "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + }, "introCarouselLabel": { "message": "Вітаємо в Bitwarden" }, @@ -5227,5 +5233,50 @@ }, "secureDevicesBody": { "message": "Зберігайте скільки завгодно паролів на необмеженій кількості пристроїв, використовуючи Bitwarden для мобільних пристроїв, браузерів та комп'ютерів." + }, + "emptyVaultNudgeTitle": { + "message": "Import existing passwords" + }, + "emptyVaultNudgeBody": { + "message": "Use the importer to quickly transfer logins to Bitwarden without manually adding them." + }, + "emptyVaultNudgeButton": { + "message": "Import now" + }, + "hasItemsVaultNudgeTitle": { + "message": "Welcome to your vault!" + }, + "hasItemsVaultNudgeBody": { + "message": "Autofill items for the current page\nFavorite items for easy access\nSearch your vault for something else" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/vi/messages.json b/apps/browser/src/_locales/vi/messages.json index c2f46945323..280a590033a 100644 --- a/apps/browser/src/_locales/vi/messages.json +++ b/apps/browser/src/_locales/vi/messages.json @@ -5201,6 +5201,12 @@ "changeAtRiskPassword": { "message": "Change at-risk password" }, + "settingsVaultOptions": { + "message": "Vault options" + }, + "emptyVaultDescription": { + "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + }, "introCarouselLabel": { "message": "Welcome to Bitwarden" }, @@ -5227,5 +5233,50 @@ }, "secureDevicesBody": { "message": "Save unlimited passwords across unlimited devices with Bitwarden mobile, browser, and desktop apps." + }, + "emptyVaultNudgeTitle": { + "message": "Import existing passwords" + }, + "emptyVaultNudgeBody": { + "message": "Use the importer to quickly transfer logins to Bitwarden without manually adding them." + }, + "emptyVaultNudgeButton": { + "message": "Import now" + }, + "hasItemsVaultNudgeTitle": { + "message": "Welcome to your vault!" + }, + "hasItemsVaultNudgeBody": { + "message": "Autofill items for the current page\nFavorite items for easy access\nSearch your vault for something else" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/zh_CN/messages.json b/apps/browser/src/_locales/zh_CN/messages.json index d11f8d7bbd7..521c72a2fb5 100644 --- a/apps/browser/src/_locales/zh_CN/messages.json +++ b/apps/browser/src/_locales/zh_CN/messages.json @@ -2180,7 +2180,7 @@ "message": "使用此用户名" }, "securePasswordGenerated": { - "message": "安全的密码生成好了!别忘了在网站上也更新一下您的密码。" + "message": "安全的密码已生成!不要忘记在网站上更新您的密码。" }, "useGeneratorHelpTextPartOne": { "message": "使用生成器", @@ -5201,6 +5201,12 @@ "changeAtRiskPassword": { "message": "更改有风险的密码" }, + "settingsVaultOptions": { + "message": "密码库选项" + }, + "emptyVaultDescription": { + "message": "密码库不仅保护您的密码。在这里还可以安全地存储登录、ID、支付卡和笔记。" + }, "introCarouselLabel": { "message": "欢迎使用 Bitwarden" }, @@ -5227,5 +5233,50 @@ }, "secureDevicesBody": { "message": "使用 Bitwarden 移动端、浏览器和桌面 App 在无限制的设备上保存无限数量的密码。" + }, + "emptyVaultNudgeTitle": { + "message": "导入现有密码" + }, + "emptyVaultNudgeBody": { + "message": "使用导入器快速将登录传输到 Bitwarden 而无需手动添加。" + }, + "emptyVaultNudgeButton": { + "message": "立即导入" + }, + "hasItemsVaultNudgeTitle": { + "message": "欢迎来到您的密码库!" + }, + "hasItemsVaultNudgeBody": { + "message": "自动填充项目用于当前页面\n收藏夹项目用于轻松访问\n搜索密码库用于其他目的" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/zh_TW/messages.json b/apps/browser/src/_locales/zh_TW/messages.json index dde0607ee18..d122f7abab2 100644 --- a/apps/browser/src/_locales/zh_TW/messages.json +++ b/apps/browser/src/_locales/zh_TW/messages.json @@ -5201,6 +5201,12 @@ "changeAtRiskPassword": { "message": "Change at-risk password" }, + "settingsVaultOptions": { + "message": "Vault options" + }, + "emptyVaultDescription": { + "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + }, "introCarouselLabel": { "message": "Welcome to Bitwarden" }, @@ -5227,5 +5233,50 @@ }, "secureDevicesBody": { "message": "Save unlimited passwords across unlimited devices with Bitwarden mobile, browser, and desktop apps." + }, + "emptyVaultNudgeTitle": { + "message": "Import existing passwords" + }, + "emptyVaultNudgeBody": { + "message": "Use the importer to quickly transfer logins to Bitwarden without manually adding them." + }, + "emptyVaultNudgeButton": { + "message": "Import now" + }, + "hasItemsVaultNudgeTitle": { + "message": "Welcome to your vault!" + }, + "hasItemsVaultNudgeBody": { + "message": "Autofill items for the current page\nFavorite items for easy access\nSearch your vault for something else" + }, + "newLoginNudgeTitle": { + "message": "Save time with autofill" + }, + "newLoginNudgeBody": { + "message": "Include a Website so this login appears as an autofill suggestion." + }, + "newCardNudgeTitle": { + "message": "Seamless online checkout" + }, + "newCardNudgeBody": { + "message": "With cards, easily autofill payment forms securely and accurately." + }, + "newIdentityNudgeTitle": { + "message": "Simplify creating accounts" + }, + "newIdentityNudgeBody": { + "message": "With identities, quickly autofill long registration or contact forms." + }, + "newNoteNudgeTitle": { + "message": "Keep your sensitive data safe" + }, + "newNoteNudgeBody": { + "message": "With notes, securely store sensitive data like banking or insurance details." + }, + "newSshNudgeTitle": { + "message": "Developer-friendly SSH access" + }, + "newSshNudgeBody": { + "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication." } -} \ No newline at end of file +} From d9fee1d1ad899ee73849c7cfa79f9bddbeb28139 Mon Sep 17 00:00:00 2001 From: ronymc Date: Fri, 2 May 2025 16:03:14 +0200 Subject: [PATCH 120/499] Fix-Reordering URIs in an item loses match detection for all URIs #14188. (#14315) Co-authored-by: SmithThe4th --- .../components/autofill-options/autofill-options.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/vault/src/cipher-form/components/autofill-options/autofill-options.component.ts b/libs/vault/src/cipher-form/components/autofill-options/autofill-options.component.ts index 5b1e4eca103..ac670a39335 100644 --- a/libs/vault/src/cipher-form/components/autofill-options/autofill-options.component.ts +++ b/libs/vault/src/cipher-form/components/autofill-options/autofill-options.component.ts @@ -239,7 +239,7 @@ export class AutofillOptionsComponent implements OnInit { (control) => Object.assign(new LoginUriView(), { uri: control.value.uri, - matchDetection: control.value.matchDetection ?? null, + match: control.value.matchDetection ?? null, }) as LoginUriView, ); return cipher; From ebe3e98a1fbf4ace7a9f14795120840d64c9427e Mon Sep 17 00:00:00 2001 From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> Date: Fri, 2 May 2025 10:11:29 -0500 Subject: [PATCH 121/499] [PM-21022] Remove fido2Credentials when cloning a cipher (#14573) * remove fido2Credentials from cipherView when cloning a cipher * add check for login on cloning cipher --- .../components/cipher-form.component.spec.ts | 39 ++++++++++++++++++- .../components/cipher-form.component.ts | 4 ++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/libs/vault/src/cipher-form/components/cipher-form.component.spec.ts b/libs/vault/src/cipher-form/components/cipher-form.component.spec.ts index f53dbfbbda1..4c61ad5d2d5 100644 --- a/libs/vault/src/cipher-form/components/cipher-form.component.spec.ts +++ b/libs/vault/src/cipher-form/components/cipher-form.component.spec.ts @@ -1,13 +1,17 @@ import { ChangeDetectorRef } from "@angular/core"; import { ComponentFixture, TestBed } from "@angular/core/testing"; import { ReactiveFormsModule } from "@angular/forms"; +import { mock } from "jest-mock-extended"; import { ViewCacheService } from "@bitwarden/angular/platform/abstractions/view-cache.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { Fido2CredentialView } from "@bitwarden/common/vault/models/view/fido2-credential.view"; import { ToastService } from "@bitwarden/components"; +import { CipherFormConfig } from "../abstractions/cipher-form-config.service"; import { CipherFormService } from "../abstractions/cipher-form.service"; import { CipherFormCacheService } from "../services/default-cipher-form-cache.service"; @@ -17,20 +21,24 @@ describe("CipherFormComponent", () => { let component: CipherFormComponent; let fixture: ComponentFixture; + const decryptCipher = jest.fn().mockResolvedValue(new CipherView()); + beforeEach(async () => { + decryptCipher.mockClear(); + await TestBed.configureTestingModule({ imports: [CipherFormComponent, ReactiveFormsModule], providers: [ { provide: ChangeDetectorRef, useValue: {} }, { provide: I18nService, useValue: { t: (key: string) => key } }, { provide: ToastService, useValue: { showToast: jest.fn() } }, - { provide: CipherFormService, useValue: { saveCipher: jest.fn() } }, + { provide: CipherFormService, useValue: { saveCipher: jest.fn(), decryptCipher } }, { provide: CipherFormCacheService, useValue: { init: jest.fn(), getCachedCipherView: jest.fn() }, }, { provide: ViewCacheService, useValue: { signal: jest.fn(() => () => null) } }, - { provide: ConfigService, useValue: {} }, + { provide: ConfigService, useValue: mock() }, ], }).compileComponents(); }); @@ -87,4 +95,31 @@ describe("CipherFormComponent", () => { expect(component.website).toEqual("https://example.com"); }); }); + + describe("clone", () => { + const cipherView = new CipherView(); + cipherView.id = "test-id"; + cipherView.login.fido2Credentials = [new Fido2CredentialView()]; + + beforeEach(() => { + component.config = { + mode: "clone", + originalCipher: new Cipher(), + } as CipherFormConfig; + + decryptCipher.mockResolvedValue(cipherView); + }); + + it("clears id on updatedCipherView", async () => { + await component.ngOnInit(); + + expect(component["updatedCipherView"]?.id).toBeNull(); + }); + + it("clears fido2Credentials on updatedCipherView", async () => { + await component.ngOnInit(); + + expect(component["updatedCipherView"]?.login.fido2Credentials).toBeNull(); + }); + }); }); diff --git a/libs/vault/src/cipher-form/components/cipher-form.component.ts b/libs/vault/src/cipher-form/components/cipher-form.component.ts index 96e1328338b..8b99b60bc16 100644 --- a/libs/vault/src/cipher-form/components/cipher-form.component.ts +++ b/libs/vault/src/cipher-form/components/cipher-form.component.ts @@ -243,6 +243,10 @@ export class CipherFormComponent implements AfterViewInit, OnInit, OnChanges, Ci if (this.config.mode === "clone") { this.updatedCipherView.id = null; + + if (this.updatedCipherView.login) { + this.updatedCipherView.login.fido2Credentials = null; + } } } else { this.updatedCipherView.type = this.config.cipherType; From e23bc6b77be8944618b4dd666946c49d38719dd5 Mon Sep 17 00:00:00 2001 From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Date: Fri, 2 May 2025 16:50:50 +0100 Subject: [PATCH 122/499] [PM-21073] No error when submitting empty form (#14597) * Resolve the space in note and colon issue * Resolve the max length for Note * Revert the changes --- .../billing/members/add-sponsorship-dialog.component.html | 5 ++--- .../app/billing/members/add-sponsorship-dialog.component.ts | 6 +++++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/apps/web/src/app/billing/members/add-sponsorship-dialog.component.html b/apps/web/src/app/billing/members/add-sponsorship-dialog.component.html index 405211d6ecb..5bcfe9a15e1 100644 --- a/apps/web/src/app/billing/members/add-sponsorship-dialog.component.html +++ b/apps/web/src/app/billing/members/add-sponsorship-dialog.component.html @@ -7,7 +7,7 @@
      - {{ "email" | i18n }}: + {{ "email" | i18n }}
      - {{ "notes" | i18n }}: + {{ "notes" | i18n }}
      diff --git a/apps/web/src/app/billing/members/add-sponsorship-dialog.component.ts b/apps/web/src/app/billing/members/add-sponsorship-dialog.component.ts index 1b044e66257..7e6c0d464c3 100644 --- a/apps/web/src/app/billing/members/add-sponsorship-dialog.component.ts +++ b/apps/web/src/app/billing/members/add-sponsorship-dialog.component.ts @@ -74,7 +74,9 @@ export class AddSponsorshipDialogComponent { asyncValidators: [this.isOrganizationMember.bind(this)], updateOn: "change", }), - sponsorshipNote: new FormControl("", {}), + sponsorshipNote: new FormControl("", { + validators: [Validators.maxLength(1000)], + }), }); } @@ -86,6 +88,8 @@ export class AddSponsorshipDialogComponent { } protected async save() { + this.sponsorshipEmailControl.markAllAsTouched(); + if (this.sponsorshipForm.invalid) { return; } From 93d4fe6d471dd2b4df607440d2a403647245c9fb Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Fri, 2 May 2025 12:18:19 -0400 Subject: [PATCH 123/499] Pass correct parameter to endpoint (#14607) --- .../app/billing/members/free-bitwarden-families.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/app/billing/members/free-bitwarden-families.component.ts b/apps/web/src/app/billing/members/free-bitwarden-families.component.ts index c141eaebd78..3fde4ded405 100644 --- a/apps/web/src/app/billing/members/free-bitwarden-families.component.ts +++ b/apps/web/src/app/billing/members/free-bitwarden-families.component.ts @@ -176,7 +176,7 @@ export class FreeBitwardenFamiliesComponent implements OnInit { return; } - await this.apiService.deleteRevokeSponsorship(sponsorship.sponsoringOrganizationUserId); + await this.apiService.deleteRevokeSponsorship(this.organizationId); this.toastService.showToast({ variant: "success", From fdfb0196d09989ee86067fe7c27c8595d90dd3bc Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Fri, 2 May 2025 18:51:01 +0200 Subject: [PATCH 124/499] [PM-20108] Update account recovery trust prompt, delete organization trust prompt (#14218) * Update trust prompt and move to shared km module * Delete organization trust component * Update org trust warning message --- apps/browser/src/_locales/en/messages.json | 24 +++++++ apps/desktop/src/locales/en/messages.json | 24 +++++++ .../manage/organization-trust.component.html | 23 ------- .../manage/organization-trust.component.ts | 69 ------------------- .../enroll-master-password-reset.component.ts | 4 +- .../src/app/shared/loose-components.module.ts | 3 - apps/web/src/locales/en/messages.json | 6 ++ .../account-recovery-trust.component.html | 4 +- 8 files changed, 59 insertions(+), 98 deletions(-) delete mode 100644 apps/web/src/app/admin-console/organizations/manage/organization-trust.component.html delete mode 100644 apps/web/src/app/admin-console/organizations/manage/organization-trust.component.ts diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 6e1e2ef57ac..2b6adfdd7e8 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -3564,6 +3564,30 @@ "deviceTrusted": { "message": "Device trusted" }, + "trustOrganization": { + "message": "Trust organization" + }, + "trust": { + "message": "Trust" + }, + "doNotTrust": { + "message": "Do not trust" + }, + "organizationNotTrusted": { + "message": "Organization is not trusted" + }, + "emergencyAccessTrustWarning": { + "message": "For the security of your account, only confirm if you have granted emergency access to this user and their fingerprint matches what is displayed in their account" + }, + "orgTrustWarning": { + "message": "For the security of your account, only proceed if you are a member of this organization, have account recovery enabled, and the fingerprint displayed below matches the organization's fingerprint." + }, + "orgTrustWarning1": { + "message": "This organization has an Enterprise policy that will enroll you in account recovery. Enrollment will allow organization administrators to change your password. Only proceed if you recognize this organization and the fingerprint phrase displayed below matches the organization's fingerprint." + }, + "trustUser":{ + "message": "Trust user" + }, "sendsNoItemsTitle": { "message": "No active Sends", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index 2350e0df4c7..7dee2c111d9 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -3152,6 +3152,30 @@ "deviceTrusted": { "message": "Device trusted" }, + "trustOrganization": { + "message": "Trust organization" + }, + "trust": { + "message": "Trust" + }, + "doNotTrust": { + "message": "Do not trust" + }, + "organizationNotTrusted": { + "message": "Organization is not trusted" + }, + "emergencyAccessTrustWarning": { + "message": "For the security of your account, only confirm if you have granted emergency access to this user and their fingerprint matches what is displayed in their account" + }, + "orgTrustWarning": { + "message": "For the security of your account, only proceed if you are a member of this organization, have account recovery enabled, and the fingerprint displayed below matches the organization's fingerprint." + }, + "orgTrustWarning1": { + "message": "This organization has an Enterprise policy that will enroll you in account recovery. Enrollment will allow organization administrators to change your password. Only proceed if you recognize this organization and the fingerprint phrase displayed below matches the organization's fingerprint." + }, + "trustUser":{ + "message": "Trust user" + }, "inputRequired": { "message": "Input is required." }, diff --git a/apps/web/src/app/admin-console/organizations/manage/organization-trust.component.html b/apps/web/src/app/admin-console/organizations/manage/organization-trust.component.html deleted file mode 100644 index 9a0310fde2a..00000000000 --- a/apps/web/src/app/admin-console/organizations/manage/organization-trust.component.html +++ /dev/null @@ -1,23 +0,0 @@ - - - - {{ "orgTrustWarning" | i18n }} -

      - {{ "fingerprintPhrase" | i18n }} {{ fingerprint }} -

      -
      - - - - -
      - diff --git a/apps/web/src/app/admin-console/organizations/manage/organization-trust.component.ts b/apps/web/src/app/admin-console/organizations/manage/organization-trust.component.ts deleted file mode 100644 index 3f013c9fc74..00000000000 --- a/apps/web/src/app/admin-console/organizations/manage/organization-trust.component.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { DialogRef, DIALOG_DATA } from "@angular/cdk/dialog"; -import { Component, OnInit, Inject } from "@angular/core"; -import { FormBuilder } from "@angular/forms"; - -import { OrganizationManagementPreferencesService } from "@bitwarden/common/admin-console/abstractions/organization-management-preferences/organization-management-preferences.service"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { DialogService } from "@bitwarden/components"; -import { KeyService } from "@bitwarden/key-management"; - -type OrganizationTrustDialogData = { - /** display name of the organization */ - name: string; - /** identifies the organization */ - orgId: string; - /** org public key */ - publicKey: Uint8Array; -}; -@Component({ - selector: "organization-trust", - templateUrl: "organization-trust.component.html", -}) -export class OrganizationTrustComponent implements OnInit { - loading = true; - fingerprint: string = ""; - confirmForm = this.formBuilder.group({}); - - constructor( - @Inject(DIALOG_DATA) protected params: OrganizationTrustDialogData, - private formBuilder: FormBuilder, - private keyService: KeyService, - protected organizationManagementPreferencesService: OrganizationManagementPreferencesService, - private logService: LogService, - private dialogRef: DialogRef, - ) {} - - async ngOnInit() { - try { - const fingerprint = await this.keyService.getFingerprint( - this.params.orgId, - this.params.publicKey, - ); - if (fingerprint != null) { - this.fingerprint = fingerprint.join("-"); - } - } catch (e) { - this.logService.error(e); - } - this.loading = false; - } - - submit = async () => { - if (this.loading) { - return; - } - - this.dialogRef.close(true); - }; - - /** - * Strongly typed helper to open a OrganizationTrustComponent - * @param dialogService Instance of the dialog service that will be used to open the dialog - * @param data The data to pass to the dialog - */ - static open(dialogService: DialogService, data: OrganizationTrustDialogData) { - return dialogService.open(OrganizationTrustComponent, { - data, - }); - } -} diff --git a/apps/web/src/app/admin-console/organizations/users/enroll-master-password-reset.component.ts b/apps/web/src/app/admin-console/organizations/users/enroll-master-password-reset.component.ts index 15e7af1cd2d..b63171ae6e6 100644 --- a/apps/web/src/app/admin-console/organizations/users/enroll-master-password-reset.component.ts +++ b/apps/web/src/app/admin-console/organizations/users/enroll-master-password-reset.component.ts @@ -18,8 +18,8 @@ import { Utils } from "@bitwarden/common/platform/misc/utils"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { DialogService, ToastService } from "@bitwarden/components"; import { KeyService } from "@bitwarden/key-management"; +import { AccountRecoveryTrustComponent } from "@bitwarden/key-management-ui"; -import { OrganizationTrustComponent } from "../manage/organization-trust.component"; import { OrganizationUserResetPasswordService } from "../members/services/organization-user-reset-password/organization-user-reset-password.service"; interface EnrollMasterPasswordResetData { @@ -62,7 +62,7 @@ export class EnrollMasterPasswordReset { await userVerificationService.buildRequest( secret, ); - const dialogRef = OrganizationTrustComponent.open(dialogService, { + const dialogRef = AccountRecoveryTrustComponent.open(dialogService, { name: data.organization.name, orgId: data.organization.id, publicKey, diff --git a/apps/web/src/app/shared/loose-components.module.ts b/apps/web/src/app/shared/loose-components.module.ts index 90e4c6ba9c3..e43c73cc3fb 100644 --- a/apps/web/src/app/shared/loose-components.module.ts +++ b/apps/web/src/app/shared/loose-components.module.ts @@ -9,7 +9,6 @@ import { LayoutComponent, NavigationModule } from "@bitwarden/components"; import { OrganizationLayoutComponent } from "../admin-console/organizations/layouts/organization-layout.component"; import { EventsComponent as OrgEventsComponent } from "../admin-console/organizations/manage/events.component"; -import { OrganizationTrustComponent } from "../admin-console/organizations/manage/organization-trust.component"; import { UserConfirmComponent as OrgUserConfirmComponent } from "../admin-console/organizations/manage/user-confirm.component"; import { VerifyRecoverDeleteOrgComponent } from "../admin-console/organizations/manage/verify-recover-delete-org.component"; import { AcceptFamilySponsorshipComponent } from "../admin-console/organizations/sponsorships/accept-family-sponsorship.component"; @@ -111,7 +110,6 @@ import { SharedModule } from "./shared.module"; OrgReusedPasswordsReportComponent, OrgUnsecuredWebsitesReportComponent, OrgUserConfirmComponent, - OrganizationTrustComponent, OrgWeakPasswordsReportComponent, PreferencesComponent, PremiumBadgeComponent, @@ -157,7 +155,6 @@ import { SharedModule } from "./shared.module"; OrgReusedPasswordsReportComponent, OrgUnsecuredWebsitesReportComponent, OrgUserConfirmComponent, - OrganizationTrustComponent, OrgWeakPasswordsReportComponent, PreferencesComponent, PremiumBadgeComponent, diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 8a37db4afda..a1d8d794f03 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -10397,12 +10397,18 @@ "doNotTrust": { "message": "Do not trust" }, + "organizationNotTrusted": { + "message": "Organization is not trusted" + }, "emergencyAccessTrustWarning": { "message": "For the security of your account, only confirm if you have granted emergency access to this user and their fingerprint matches what is displayed in their account" }, "orgTrustWarning": { "message": "For the security of your account, only proceed if you are a member of this organization, have account recovery enabled, and the fingerprint displayed below matches the organization's fingerprint." }, + "orgTrustWarning1": { + "message": "This organization has an Enterprise policy that will enroll you in account recovery. Enrollment will allow organization administrators to change your password. Only proceed if you recognize this organization and the fingerprint phrase displayed below matches the organization's fingerprint." + }, "trustUser":{ "message": "Trust user" }, diff --git a/libs/key-management-ui/src/trust/account-recovery-trust.component.html b/libs/key-management-ui/src/trust/account-recovery-trust.component.html index 5829558dd69..6d4c5b34dc2 100644 --- a/libs/key-management-ui/src/trust/account-recovery-trust.component.html +++ b/libs/key-management-ui/src/trust/account-recovery-trust.component.html @@ -5,8 +5,10 @@ [subtitle]="params.name" > - {{ "orgTrustWarning" | i18n }}

      + {{ "orgTrustWarning1" | i18n }} +
      +
      {{ "fingerprintPhrase" | i18n }} {{ fingerprint }}

      From 237002b6332165f40664e767c6886b993fd2a2eb Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Fri, 2 May 2025 14:21:08 -0700 Subject: [PATCH 125/499] [PM-20988][PM-20986][PM-20983][PM-20971][PM-21019] - Multiple defect fixes for desktop cipher form update (#14559) * multiple bug fixes * favor getters to local state * fix tests --- apps/desktop/src/locales/en/messages.json | 15 ++++++++++++++ .../app/vault/item-footer.component.html | 2 +- .../src/vault/app/vault/vault-v2.component.ts | 10 +++++++--- .../view-identity-sections.component.html | 6 +++--- .../view-identity-sections.component.spec.ts | 7 ------- .../view-identity-sections.component.ts | 20 +++++-------------- 6 files changed, 31 insertions(+), 29 deletions(-) diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index 7dee2c111d9..e685c053529 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -2007,6 +2007,21 @@ "message": "Premium", "description": "Premium membership" }, + "freeOrgsCannotUseAttachments": { + "message": "Free organizations cannot use attachments" + }, + "singleFieldNeedsAttention": { + "message": "1 field needs your attention." + }, + "multipleFieldsNeedAttention": { + "message": "$COUNT$ fields need your attention.", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, "cardExpiredTitle": { "message": "Expired card" }, diff --git a/apps/desktop/src/vault/app/vault/item-footer.component.html b/apps/desktop/src/vault/app/vault/item-footer.component.html index 6915555c08b..824a2d481cf 100644 --- a/apps/desktop/src/vault/app/vault/item-footer.component.html +++ b/apps/desktop/src/vault/app/vault/item-footer.component.html @@ -44,7 +44,7 @@
      +
      + + + {{ + "sshAgentPromptBehaviorDesc" | i18n + }} +
      @@ -88,30 +73,15 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the inline menu with
      @@ -139,30 +109,15 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the inline menu with
      @@ -190,30 +145,15 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the inline menu with
    @@ -248,13 +188,19 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers f aria-hidden="true" fill="none" height="24" - viewBox="0 0 23 24" - width="23" + viewBox="0 0 24 24" + width="24" xmlns="http://www.w3.org/2000/svg" > + @@ -283,29 +229,23 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers f
@@ -323,30 +263,15 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers f
@@ -404,29 +329,23 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers f
@@ -473,29 +392,23 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers f
@@ -528,29 +441,23 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers f
@@ -575,15 +482,15 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers f @@ -613,29 +520,23 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers f
@@ -683,29 +584,23 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers f
@@ -753,29 +648,23 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers f
@@ -876,29 +765,23 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers f

@@ -958,29 +841,23 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers f @@ -1027,29 +904,23 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers f @@ -1082,29 +953,23 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers f @@ -1129,15 +994,15 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers f @@ -1167,29 +1032,23 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers f @@ -1237,29 +1096,23 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers f @@ -1307,29 +1160,23 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers f @@ -1389,29 +1236,23 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers f @@ -1458,29 +1299,23 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers f @@ -1513,29 +1348,23 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers f @@ -1560,15 +1389,15 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers f @@ -1598,29 +1427,23 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers f @@ -1668,29 +1491,23 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers f @@ -1738,29 +1555,23 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers f @@ -1814,19 +1625,15 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers f @@ -1842,29 +1649,23 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers f @@ -1903,19 +1704,15 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers f @@ -1931,29 +1728,23 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers f @@ -1992,19 +1783,15 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers f @@ -2026,29 +1813,23 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers f @@ -2099,29 +1880,23 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers f @@ -2169,29 +1944,23 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers f @@ -2239,29 +2008,23 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers f @@ -2287,10 +2050,10 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers f > @@ -2407,29 +2170,23 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers f @@ -2525,29 +2282,23 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers f @@ -2578,29 +2329,21 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the locked inline men unlockAccount @@ -2623,17 +2366,22 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the password generato diff --git a/apps/browser/src/autofill/popup/fido2/fido2.component.html b/apps/browser/src/autofill/popup/fido2/fido2.component.html index 80ea6726cb9..8d8394641e9 100644 --- a/apps/browser/src/autofill/popup/fido2/fido2.component.html +++ b/apps/browser/src/autofill/popup/fido2/fido2.component.html @@ -14,7 +14,7 @@ (click)="addCipher()" slot="end" > - + {{ "new" | i18n }} diff --git a/apps/browser/src/autofill/utils/svg-icons.ts b/apps/browser/src/autofill/utils/svg-icons.ts index b04d18608ec..343acc00b06 100644 --- a/apps/browser/src/autofill/utils/svg-icons.ts +++ b/apps/browser/src/autofill/utils/svg-icons.ts @@ -5,34 +5,34 @@ export const logoLockedIcon = ''; export const globeIcon = - ''; + ''; export const creditCardIcon = - ''; + ''; export const idCardIcon = - ''; + ''; export const lockIcon = - ''; + ''; export const plusIcon = - ''; + ''; export const viewCipherIcon = - ''; + ''; export const passkeyIcon = - ''; + ''; export const circleCheckIcon = - ''; + ''; export const spinnerIcon = - ''; + ''; export const keyIcon = - ''; + ''; export const refreshIcon = - ''; + ''; diff --git a/apps/browser/src/platform/popup/layout/popup-header.component.html b/apps/browser/src/platform/popup/layout/popup-header.component.html index ac52a630900..c58bc258bf6 100644 --- a/apps/browser/src/platform/popup/layout/popup-header.component.html +++ b/apps/browser/src/platform/popup/layout/popup-header.component.html @@ -11,7 +11,7 @@
`, @@ -405,26 +406,26 @@ const navButtons = (showBerry = false) => [ { label: "vault", page: "/tabs/vault", - iconKey: "lock", - iconKeyActive: "lock-f", + icon: Icons.VaultInactive, + iconActive: Icons.VaultActive, }, { label: "generator", page: "/tabs/generator", - iconKey: "generate", - iconKeyActive: "generate-f", + icon: Icons.GeneratorInactive, + iconActive: Icons.GeneratorActive, }, { label: "send", page: "/tabs/send", - iconKey: "send", - iconKeyActive: "send-f", + icon: Icons.SendInactive, + iconActive: Icons.SendActive, }, { label: "settings", page: "/tabs/settings", - iconKey: "cog", - iconKeyActive: "cog-f", + icon: Icons.SettingsInactive, + iconActive: Icons.SettingsActive, showBerry: showBerry, }, ]; diff --git a/apps/browser/src/platform/popup/layout/popup-tab-navigation.component.html b/apps/browser/src/platform/popup/layout/popup-tab-navigation.component.html index 27b546738c3..1170725a4b7 100644 --- a/apps/browser/src/platform/popup/layout/popup-tab-navigation.component.html +++ b/apps/browser/src/platform/popup/layout/popup-tab-navigation.component.html @@ -7,7 +7,7 @@
  • diff --git a/apps/browser/src/platform/popup/layout/popup-tab-navigation.component.ts b/apps/browser/src/platform/popup/layout/popup-tab-navigation.component.ts index f4b82dc56fc..4984d3749a1 100644 --- a/apps/browser/src/platform/popup/layout/popup-tab-navigation.component.ts +++ b/apps/browser/src/platform/popup/layout/popup-tab-navigation.component.ts @@ -4,13 +4,13 @@ import { RouterModule } from "@angular/router"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { LinkModule } from "@bitwarden/components"; +import { Icon, IconModule, LinkModule } from "@bitwarden/components"; export type NavButton = { label: string; page: string; - iconKey: string; - iconKeyActive: string; + icon: Icon; + iconActive: Icon; showBerry?: boolean; }; @@ -18,7 +18,7 @@ export type NavButton = { selector: "popup-tab-navigation", templateUrl: "popup-tab-navigation.component.html", standalone: true, - imports: [CommonModule, LinkModule, RouterModule, JslibModule], + imports: [CommonModule, LinkModule, RouterModule, JslibModule, IconModule], host: { class: "tw-block tw-h-full tw-w-full tw-flex tw-flex-col", }, diff --git a/apps/browser/src/popup/tabs-v2.component.ts b/apps/browser/src/popup/tabs-v2.component.ts index f5b56cf829f..63b539fddce 100644 --- a/apps/browser/src/popup/tabs-v2.component.ts +++ b/apps/browser/src/popup/tabs-v2.component.ts @@ -1,70 +1,60 @@ -import { Component, OnInit } from "@angular/core"; -import { combineLatest, firstValueFrom, map, Observable } from "rxjs"; +import { Component } from "@angular/core"; +import { combineLatest, map, Observable, switchMap } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; -import { UserId } from "@bitwarden/common/types/guid"; +import { Icons } from "@bitwarden/components"; import { VaultNudgesService } from "@bitwarden/vault"; +import { NavButton } from "../platform/popup/layout/popup-tab-navigation.component"; + @Component({ selector: "app-tabs-v2", templateUrl: "./tabs-v2.component.html", }) -export class TabsV2Component implements OnInit { - private activeUserId: UserId | null = null; - protected navButtons$: Observable< - { - label: string; - page: string; - iconKey: string; - iconKeyActive: string; - showBerry?: boolean; - }[] - > = new Observable(); +export class TabsV2Component { + private hasActiveBadges$ = this.accountService.activeAccount$ + .pipe(getUserId) + .pipe(switchMap((userId) => this.vaultNudgesService.hasActiveBadges$(userId))); + protected navButtons$: Observable = combineLatest([ + this.configService.getFeatureFlag$(FeatureFlag.PM8851_BrowserOnboardingNudge), + this.hasActiveBadges$, + ]).pipe( + map(([onboardingFeatureEnabled, hasBadges]) => { + return [ + { + label: "vault", + page: "/tabs/vault", + icon: Icons.VaultInactive, + iconActive: Icons.VaultActive, + }, + { + label: "generator", + page: "/tabs/generator", + icon: Icons.GeneratorInactive, + iconActive: Icons.GeneratorActive, + }, + { + label: "send", + page: "/tabs/send", + icon: Icons.SendInactive, + iconActive: Icons.SendActive, + }, + { + label: "settings", + page: "/tabs/settings", + icon: Icons.SettingsInactive, + iconActive: Icons.SettingsActive, + showBerry: onboardingFeatureEnabled && hasBadges, + }, + ]; + }), + ); constructor( private vaultNudgesService: VaultNudgesService, private accountService: AccountService, private readonly configService: ConfigService, ) {} - - async ngOnInit() { - this.activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); - - this.navButtons$ = combineLatest([ - this.configService.getFeatureFlag$(FeatureFlag.PM8851_BrowserOnboardingNudge), - this.vaultNudgesService.hasActiveBadges$(this.activeUserId), - ]).pipe( - map(([onboardingFeatureEnabled, hasBadges]) => { - return [ - { - label: "vault", - page: "/tabs/vault", - iconKey: "lock", - iconKeyActive: "lock-f", - }, - { - label: "generator", - page: "/tabs/generator", - iconKey: "generate", - iconKeyActive: "generate-f", - }, - { - label: "send", - page: "/tabs/send", - iconKey: "send", - iconKeyActive: "send-f", - }, - { - label: "settings", - page: "/tabs/settings", - iconKey: "cog", - iconKeyActive: "cog-f", - showBerry: onboardingFeatureEnabled && hasBadges, - }, - ]; - }), - ); - } } diff --git a/apps/browser/src/tools/popup/settings/settings-v2.component.html b/apps/browser/src/tools/popup/settings/settings-v2.component.html index 6189221942c..ab61e5b10f8 100644 --- a/apps/browser/src/tools/popup/settings/settings-v2.component.html +++ b/apps/browser/src/tools/popup/settings/settings-v2.component.html @@ -54,7 +54,7 @@ - + {{ "appearance" | i18n }} diff --git a/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.html b/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.html index af627e22ef2..6b6e8728f19 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.html +++ b/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.html @@ -1,5 +1,5 @@ diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-list-filters/vault-list-filters.component.html b/apps/browser/src/vault/popup/components/vault-v2/vault-list-filters/vault-list-filters.component.html index c61562f9f90..ba4cbf71251 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-list-filters/vault-list-filters.component.html +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-list-filters/vault-list-filters.component.html @@ -20,7 +20,7 @@ *ngIf="collections.length" fullWidth formControlName="collection" - placeholderIcon="bwi-collection" + placeholderIcon="bwi-collection-shared" [placeholderText]="'collection' | i18n" [options]="collections" > diff --git a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts index f9785bccd00..9498d953808 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts @@ -362,7 +362,7 @@ describe("VaultPopupListFiltersService", () => { it("sets collection icon", (done) => { service.collections$.subscribe((collections) => { - expect(collections.every(({ icon }) => icon === "bwi-collection")).toBeTruthy(); + expect(collections.every(({ icon }) => icon === "bwi-collection-shared")).toBeTruthy(); done(); }); }); diff --git a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts index f11fa0f63f0..c726678c973 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts @@ -461,7 +461,7 @@ export class VaultPopupListFiltersService { }); }), map((collections) => - collections.nestedList.map((c) => this.convertToChipSelectOption(c, "bwi-collection")), + collections.nestedList.map((c) => this.convertToChipSelectOption(c, "bwi-collection-shared")), ), shareReplay({ refCount: true, bufferSize: 1 }), ); diff --git a/apps/browser/src/vault/popup/settings/folders-v2.component.html b/apps/browser/src/vault/popup/settings/folders-v2.component.html index 552547c0230..8cea05f9c17 100644 --- a/apps/browser/src/vault/popup/settings/folders-v2.component.html +++ b/apps/browser/src/vault/popup/settings/folders-v2.component.html @@ -8,7 +8,7 @@ type="button" (click)="openAddEditFolderDialog()" > - + {{ "new" | i18n }} @@ -47,7 +47,7 @@ (click)="openAddEditFolderDialog()" data-testid="empty-new-folder-button" > - + {{ "newFolder" | i18n }} diff --git a/apps/desktop/src/app/layout/nav.component.ts b/apps/desktop/src/app/layout/nav.component.ts index dff35ec12c3..dbc399c051d 100644 --- a/apps/desktop/src/app/layout/nav.component.ts +++ b/apps/desktop/src/app/layout/nav.component.ts @@ -14,12 +14,12 @@ export class NavComponent { items: any[] = [ { link: "/vault", - icon: "bwi-lock-f", + icon: "bwi-vault", label: this.i18nService.translate("myVault"), }, { link: "/send", - icon: "bwi-send-f", + icon: "bwi-send", label: "Send", }, ]; diff --git a/apps/desktop/src/vault/app/vault/add-edit-custom-fields.component.html b/apps/desktop/src/vault/app/vault/add-edit-custom-fields.component.html index c7efaad70ba..bc0c3876b71 100644 --- a/apps/desktop/src/vault/app/vault/add-edit-custom-fields.component.html +++ b/apps/desktop/src/vault/app/vault/add-edit-custom-fields.component.html @@ -110,7 +110,7 @@ *ngIf="!(!cipher.edit && editMode)" cdkDragHandle > - +
diff --git a/apps/desktop/src/vault/app/vault/add-edit.component.html b/apps/desktop/src/vault/app/vault/add-edit.component.html index 7904d7b7f34..8457e72bdc1 100644 --- a/apps/desktop/src/vault/app/vault/add-edit.component.html +++ b/apps/desktop/src/vault/app/vault/add-edit.component.html @@ -519,7 +519,7 @@ appA11yTitle="{{ 'importSshKeyFromClipboard' | i18n }}" (click)="importSshKeyFromClipboard()" > - + diff --git a/apps/desktop/src/vault/app/vault/vault-filter/filters/collection-filter.component.html b/apps/desktop/src/vault/app/vault/vault-filter/filters/collection-filter.component.html index 667cdf6798c..e123f4400c7 100644 --- a/apps/desktop/src/vault/app/vault/vault-filter/filters/collection-filter.component.html +++ b/apps/desktop/src/vault/app/vault/vault-filter/filters/collection-filter.component.html @@ -55,7 +55,7 @@ >  {{ c.node.name }} diff --git a/apps/desktop/src/vault/app/vault/vault-items.component.html b/apps/desktop/src/vault/app/vault/vault-items.component.html index b64bdaa1c80..8a869cd2a32 100644 --- a/apps/desktop/src/vault/app/vault/vault-items.component.html +++ b/apps/desktop/src/vault/app/vault/vault-items.component.html @@ -28,7 +28,7 @@ {{ c.name }} diff --git a/apps/web/src/app/admin-console/organizations/collections/vault-filter/vault-filter.component.ts b/apps/web/src/app/admin-console/organizations/collections/vault-filter/vault-filter.component.ts index 384390d738e..13e90d5275c 100644 --- a/apps/web/src/app/admin-console/organizations/collections/vault-filter/vault-filter.component.ts +++ b/apps/web/src/app/admin-console/organizations/collections/vault-filter/vault-filter.component.ts @@ -105,14 +105,14 @@ export class VaultFilterComponent id: "AllCollections", name: "collections", type: "all", - icon: "bwi-collection", + icon: "bwi-collection-shared", }, [ { id: "AllCollections", name: "Collections", type: "all", - icon: "bwi-collection", + icon: "bwi-collection-shared", }, ], ), diff --git a/apps/web/src/app/admin-console/organizations/collections/vault-header/vault-header.component.html b/apps/web/src/app/admin-console/organizations/collections/vault-header/vault-header.component.html index c2e80b7524f..3ef48ff9015 100644 --- a/apps/web/src/app/admin-console/organizations/collections/vault-header/vault-header.component.html +++ b/apps/web/src/app/admin-console/organizations/collections/vault-header/vault-header.component.html @@ -14,7 +14,7 @@ - + {{ "new" | i18n }} @@ -140,7 +140,7 @@ diff --git a/apps/web/src/app/admin-console/organizations/collections/vault-header/vault-header.component.ts b/apps/web/src/app/admin-console/organizations/collections/vault-header/vault-header.component.ts index 7efb79ebdb6..4c129e325c5 100644 --- a/apps/web/src/app/admin-console/organizations/collections/vault-header/vault-header.component.ts +++ b/apps/web/src/app/admin-console/organizations/collections/vault-header/vault-header.component.ts @@ -117,7 +117,7 @@ export class VaultHeaderComponent { } get icon() { - return this.filter.collectionId !== undefined ? "bwi-collection" : ""; + return this.filter.collectionId !== undefined ? "bwi-collection-shared" : ""; } protected get showBreadcrumbs() { diff --git a/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.html b/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.html index ad3879b3659..4e7c2403893 100644 --- a/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.html +++ b/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.html @@ -13,7 +13,7 @@ >
{{ "members" | i18n }}