From 0544100e830b16166711b05302abce9a46e193f8 Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Mon, 2 Dec 2024 09:12:49 -0500 Subject: [PATCH 01/46] Make payment optional trial banner work with Stripe sources API deprecation (#12146) --- .../vault/individual-vault/vault.component.ts | 12 +-- .../app/vault/org-vault/vault.component.ts | 12 ++- .../overview/overview.component.ts | 13 ++- .../src/services/jslib-services.module.ts | 2 + .../organization-billing.service.ts | 11 ++- .../services/organization-billing.service.ts | 83 ++++++++++++------- 6 files changed, 82 insertions(+), 51 deletions(-) diff --git a/apps/web/src/app/vault/individual-vault/vault.component.ts b/apps/web/src/app/vault/individual-vault/vault.component.ts index ce61cd51fcd..9d235784d05 100644 --- a/apps/web/src/app/vault/individual-vault/vault.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault.component.ts @@ -32,11 +32,11 @@ import { } from "rxjs/operators"; import { - Unassigned, - CollectionService, CollectionData, CollectionDetailsResponse, + CollectionService, CollectionView, + Unassigned, } from "@bitwarden/admin-console/common"; import { SearchPipe } from "@bitwarden/angular/pipes/search.pipe"; import { ModalService } from "@bitwarden/angular/services/modal.service"; @@ -47,6 +47,7 @@ import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-conso import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing/abstractions"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction"; import { EventType } from "@bitwarden/common/enums"; @@ -241,6 +242,7 @@ export class VaultComponent implements OnInit, OnDestroy { private organizationApiService: OrganizationApiServiceAbstraction, protected billingApiService: BillingApiServiceAbstraction, private trialFlowService: TrialFlowService, + private organizationBillingService: OrganizationBillingServiceAbstraction, ) {} async ngOnInit() { @@ -437,13 +439,13 @@ export class VaultComponent implements OnInit, OnDestroy { .map((org) => combineLatest([ this.organizationApiService.getSubscription(org.id), - this.organizationApiService.getBilling(org.id), + this.organizationBillingService.getPaymentSource(org.id), ]).pipe( - map(([subscription, billing]) => { + map(([subscription, paymentSource]) => { return this.trialFlowService.checkForOrgsWithUpcomingPaymentIssues( org, subscription, - billing?.paymentSource, + paymentSource, ); }), ), diff --git a/apps/web/src/app/vault/org-vault/vault.component.ts b/apps/web/src/app/vault/org-vault/vault.component.ts index 64318047b9e..18cc6e49abc 100644 --- a/apps/web/src/app/vault/org-vault/vault.component.ts +++ b/apps/web/src/app/vault/org-vault/vault.component.ts @@ -48,6 +48,7 @@ import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing/abstractions"; import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction"; import { EventType } from "@bitwarden/common/enums"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; @@ -252,6 +253,7 @@ export class VaultComponent implements OnInit, OnDestroy { private organizationApiService: OrganizationApiServiceAbstraction, private trialFlowService: TrialFlowService, protected billingApiService: BillingApiServiceAbstraction, + private organizationBillingService: OrganizationBillingServiceAbstraction, ) {} async ngOnInit() { @@ -595,15 +597,11 @@ export class VaultComponent implements OnInit, OnDestroy { combineLatest([ of(org), this.organizationApiService.getSubscription(org.id), - this.organizationApiService.getBilling(org.id), + this.organizationBillingService.getPaymentSource(org.id), ]), ), - map(([org, sub, billing]) => { - return this.trialFlowService.checkForOrgsWithUpcomingPaymentIssues( - org, - sub, - billing?.paymentSource, - ); + map(([org, sub, paymentSource]) => { + return this.trialFlowService.checkForOrgsWithUpcomingPaymentIssues(org, sub, paymentSource); }), ); diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/overview/overview.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/overview/overview.component.ts index bf2dbb76ad3..3585f09faf6 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/overview/overview.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/overview/overview.component.ts @@ -20,6 +20,7 @@ import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing/abstractions"; 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"; @@ -114,9 +115,9 @@ export class OverviewComponent implements OnInit, OnDestroy { private smOnboardingTasksService: SMOnboardingTasksService, private logService: LogService, private router: Router, - private organizationApiService: OrganizationApiServiceAbstraction, private trialFlowService: TrialFlowService, + private organizationBillingService: OrganizationBillingServiceAbstraction, ) {} ngOnInit() { @@ -144,15 +145,11 @@ export class OverviewComponent implements OnInit, OnDestroy { combineLatest([ of(org), this.organizationApiService.getSubscription(org.id), - this.organizationApiService.getBilling(org.id), + this.organizationBillingService.getPaymentSource(org.id), ]), ), - map(([org, sub, billing]) => { - return this.trialFlowService.checkForOrgsWithUpcomingPaymentIssues( - org, - sub, - billing?.paymentSource, - ); + map(([org, sub, paymentSource]) => { + return this.trialFlowService.checkForOrgsWithUpcomingPaymentIssues(org, sub, paymentSource); }), takeUntil(this.destroy$), ); diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 0208a3cdc7a..a43f1fa07a8 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -1213,6 +1213,8 @@ const safeProviders: SafeProvider[] = [ useClass: OrganizationBillingService, deps: [ ApiServiceAbstraction, + BillingApiServiceAbstraction, + ConfigService, KeyServiceAbstraction, EncryptService, I18nServiceAbstraction, diff --git a/libs/common/src/billing/abstractions/organization-billing.service.ts b/libs/common/src/billing/abstractions/organization-billing.service.ts index 72902baa30e..0bc1f3bc558 100644 --- a/libs/common/src/billing/abstractions/organization-billing.service.ts +++ b/libs/common/src/billing/abstractions/organization-billing.service.ts @@ -1,3 +1,6 @@ +import { BillingSourceResponse } from "@bitwarden/common/billing/models/response/billing.response"; +import { PaymentSourceResponse } from "@bitwarden/common/billing/models/response/payment-source.response"; + import { OrganizationResponse } from "../../admin-console/models/response/organization.response"; import { InitiationPath } from "../../models/request/reference-event.request"; import { PaymentMethodType, PlanType } from "../enums"; @@ -41,11 +44,15 @@ export type SubscriptionInformation = { }; export abstract class OrganizationBillingServiceAbstraction { - purchaseSubscription: (subscription: SubscriptionInformation) => Promise; + getPaymentSource: ( + organizationId: string, + ) => Promise; - startFree: (subscription: SubscriptionInformation) => Promise; + purchaseSubscription: (subscription: SubscriptionInformation) => Promise; purchaseSubscriptionNoPaymentMethod: ( subscription: SubscriptionInformation, ) => Promise; + + startFree: (subscription: SubscriptionInformation) => Promise; } diff --git a/libs/common/src/billing/services/organization-billing.service.ts b/libs/common/src/billing/services/organization-billing.service.ts index efc36278532..487098620bd 100644 --- a/libs/common/src/billing/services/organization-billing.service.ts +++ b/libs/common/src/billing/services/organization-billing.service.ts @@ -1,4 +1,18 @@ -import { KeyService } from "../../../../key-management/src/abstractions/key.service"; +import { + BillingApiServiceAbstraction, + OrganizationBillingServiceAbstraction, + OrganizationInformation, + PaymentInformation, + PlanInformation, + SubscriptionInformation, +} from "@bitwarden/common/billing/abstractions"; +import { BillingSourceResponse } from "@bitwarden/common/billing/models/response/billing.response"; +import { PaymentSourceResponse } from "@bitwarden/common/billing/models/response/payment-source.response"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { SyncService } from "@bitwarden/common/platform/sync"; +import { KeyService } from "@bitwarden/key-management"; + import { ApiService } from "../../abstractions/api.service"; import { OrganizationApiServiceAbstraction as OrganizationApiService } from "../../admin-console/abstractions/organization/organization-api.service.abstraction"; import { OrganizationCreateRequest } from "../../admin-console/models/request/organization-create.request"; @@ -8,14 +22,6 @@ import { EncryptService } from "../../platform/abstractions/encrypt.service"; import { I18nService } from "../../platform/abstractions/i18n.service"; import { EncString } from "../../platform/models/domain/enc-string"; import { OrgKey } from "../../types/key"; -import { SyncService } from "../../vault/abstractions/sync/sync.service.abstraction"; -import { - OrganizationBillingServiceAbstraction, - OrganizationInformation, - PaymentInformation, - PlanInformation, - SubscriptionInformation, -} from "../abstractions/organization-billing.service"; import { PlanType } from "../enums"; import { OrganizationNoPaymentMethodCreateRequest } from "../models/request/organization-no-payment-method-create-request"; @@ -29,6 +35,8 @@ interface OrganizationKeys { export class OrganizationBillingService implements OrganizationBillingServiceAbstraction { constructor( private apiService: ApiService, + private billingApiService: BillingApiServiceAbstraction, + private configService: ConfigService, private keyService: KeyService, private encryptService: EncryptService, private i18nService: I18nService, @@ -36,6 +44,23 @@ export class OrganizationBillingService implements OrganizationBillingServiceAbs private syncService: SyncService, ) {} + async getPaymentSource( + organizationId: string, + ): Promise { + const deprecateStripeSourcesAPI = await this.configService.getFeatureFlag( + FeatureFlag.AC2476_DeprecateStripeSourcesAPI, + ); + + if (deprecateStripeSourcesAPI) { + const paymentMethod = + await this.billingApiService.getOrganizationPaymentMethod(organizationId); + return paymentMethod.paymentSource; + } else { + const billing = await this.organizationApiService.getBilling(organizationId); + return billing.paymentSource; + } + } + async purchaseSubscription(subscription: SubscriptionInformation): Promise { const request = new OrganizationCreateRequest(); @@ -58,26 +83,6 @@ export class OrganizationBillingService implements OrganizationBillingServiceAbs return response; } - async startFree(subscription: SubscriptionInformation): Promise { - const request = new OrganizationCreateRequest(); - - const organizationKeys = await this.makeOrganizationKeys(); - - this.setOrganizationKeys(request, organizationKeys); - - this.setOrganizationInformation(request, subscription.organization); - - this.setPlanInformation(request, subscription.plan); - - const response = await this.organizationApiService.create(request); - - await this.apiService.refreshIdentityToken(); - - await this.syncService.fullSync(true); - - return response; - } - async purchaseSubscriptionNoPaymentMethod( subscription: SubscriptionInformation, ): Promise { @@ -100,6 +105,26 @@ export class OrganizationBillingService implements OrganizationBillingServiceAbs return response; } + async startFree(subscription: SubscriptionInformation): Promise { + const request = new OrganizationCreateRequest(); + + const organizationKeys = await this.makeOrganizationKeys(); + + this.setOrganizationKeys(request, organizationKeys); + + this.setOrganizationInformation(request, subscription.organization); + + this.setPlanInformation(request, subscription.plan); + + const response = await this.organizationApiService.create(request); + + await this.apiService.refreshIdentityToken(); + + await this.syncService.fullSync(true); + + return response; + } + private async makeOrganizationKeys(): Promise { const [encryptedKey, key] = await this.keyService.makeOrgKey(); const [publicKey, encryptedPrivateKey] = await this.keyService.makeKeyPair(key); From 456c516a6e1aa445564b5219de4967f5b75f0a4e Mon Sep 17 00:00:00 2001 From: Merissa Weinstein Date: Mon, 2 Dec 2024 08:48:31 -0600 Subject: [PATCH 02/46] update release datae for the onboarding module (#12210) --- .../src/vault/popup/services/vault-ui-onboarding.service.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/browser/src/vault/popup/services/vault-ui-onboarding.service.ts b/apps/browser/src/vault/popup/services/vault-ui-onboarding.service.ts index 151f8517d57..2b37b26b9cb 100644 --- a/apps/browser/src/vault/popup/services/vault-ui-onboarding.service.ts +++ b/apps/browser/src/vault/popup/services/vault-ui-onboarding.service.ts @@ -24,8 +24,7 @@ export const GLOBAL_VAULT_UI_ONBOARDING = new KeyDefinition( @Injectable() export class VaultUiOnboardingService { - // TODO: Update this date to the release date of the new Browser UI - private onboardingUiReleaseDate = new Date("2024-07-25"); + private onboardingUiReleaseDate = new Date("2024-12-10"); private vaultUiOnboardingState: GlobalState = this.stateProvider.getGlobal( GLOBAL_VAULT_UI_ONBOARDING, From 0ff48aa34581ff5111b7d918f7062aa655a56301 Mon Sep 17 00:00:00 2001 From: cd-bitwarden <106776772+cd-bitwarden@users.noreply.github.com> Date: Mon, 2 Dec 2024 11:30:38 -0500 Subject: [PATCH 03/46] [PM-12743] a11y changes to make new drop down list for send and vault accessible (#11717) * updating new menus to allow tab + enter to submit the link/button * Updating New actions to use button instead of a for accessibiity purposes * refactor * refactor * test fix * fixes * fixing tests * fixing test * fixing tests --------- Co-authored-by: --global <> --- .../new-item-dropdown-v2.component.html | 16 +- .../new-item-dropdown-v2.component.spec.ts | 212 ++++++++++-------- .../new-item-dropdown-v2.component.ts | 30 ++- .../new-send-dropdown.component.html | 12 +- .../new-send-dropdown.component.ts | 14 +- 5 files changed, 163 insertions(+), 121 deletions(-) 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 78403784f46..7b31e647bf0 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 @@ -3,19 +3,27 @@ {{ "new" | i18n }} - + {{ "typeLogin" | i18n }} - + {{ "typeCard" | i18n }} - + {{ "typeIdentity" | i18n }} - + {{ "note" | i18n }} diff --git a/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.spec.ts b/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.spec.ts index 6842f35ea6f..b5dc2a2f034 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.spec.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.spec.ts @@ -1,141 +1,163 @@ import { CommonModule } from "@angular/common"; import { ComponentFixture, TestBed } from "@angular/core/testing"; -import { Router } from "@angular/router"; +import { ActivatedRoute, RouterLink } from "@angular/router"; +import { mock } from "jest-mock-extended"; -import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { FolderApiServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder-api.service.abstraction"; +import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { CipherType } from "@bitwarden/common/vault/enums"; -import { ButtonModule, DialogService, MenuModule } from "@bitwarden/components"; +import { ButtonModule, DialogService, MenuModule, NoItemsModule } from "@bitwarden/components"; import { BrowserApi } from "../../../../../platform/browser/browser-api"; import BrowserPopupUtils from "../../../../../platform/popup/browser-popup-utils"; -import { AddEditQueryParams } from "../add-edit/add-edit-v2.component"; -import { AddEditFolderDialogComponent } from "../add-edit-folder-dialog/add-edit-folder-dialog.component"; import { NewItemDropdownV2Component, NewItemInitialValues } from "./new-item-dropdown-v2.component"; describe("NewItemDropdownV2Component", () => { let component: NewItemDropdownV2Component; let fixture: ComponentFixture; - const open = jest.fn(); - const navigate = jest.fn(); + let dialogServiceMock: jest.Mocked; + let browserApiMock: jest.Mocked; - jest - .spyOn(BrowserApi, "getTabFromCurrentWindow") - .mockResolvedValue({ url: "https://example.com" } as chrome.tabs.Tab); + const mockTab = { url: "https://example.com" }; + + beforeAll(() => { + jest.spyOn(BrowserApi, "getTabFromCurrentWindow").mockResolvedValue(mockTab as chrome.tabs.Tab); + jest.spyOn(BrowserPopupUtils, "inPopout").mockReturnValue(false); + jest.spyOn(Utils, "getHostname").mockReturnValue("example.com"); + }); beforeEach(async () => { - open.mockClear(); - navigate.mockClear(); + dialogServiceMock = mock(); + dialogServiceMock.open.mockClear(); + + const activatedRouteMock = { + snapshot: { paramMap: { get: jest.fn() } }, + }; + + const i18nServiceMock = mock(); + const folderServiceMock = mock(); + const folderApiServiceAbstractionMock = mock(); + const accountServiceMock = mock(); await TestBed.configureTestingModule({ - imports: [NewItemDropdownV2Component, MenuModule, ButtonModule, JslibModule, CommonModule], - providers: [ - { provide: I18nService, useValue: { t: (key: string) => key } }, - { provide: Router, useValue: { navigate } }, + imports: [ + CommonModule, + RouterLink, + ButtonModule, + MenuModule, + NoItemsModule, + NewItemDropdownV2Component, ], - }) - .overrideProvider(DialogService, { useValue: { open } }) - .compileComponents(); + providers: [ + { provide: DialogService, useValue: dialogServiceMock }, + { provide: I18nService, useValue: i18nServiceMock }, + { provide: ActivatedRoute, useValue: activatedRouteMock }, + { provide: BrowserApi, useValue: browserApiMock }, + { provide: FolderService, useValue: folderServiceMock }, + { provide: FolderApiServiceAbstraction, useValue: folderApiServiceAbstractionMock }, + { provide: AccountService, useValue: accountServiceMock }, + ], + }).compileComponents(); + }); + beforeEach(() => { fixture = TestBed.createComponent(NewItemDropdownV2Component); component = fixture.componentInstance; fixture.detectChanges(); }); - it("opens new folder dialog", () => { - component.openFolderDialog(); - - expect(open).toHaveBeenCalledWith(AddEditFolderDialogComponent); - }); - - describe("new item", () => { - const emptyParams: AddEditQueryParams = { - collectionId: undefined, - organizationId: undefined, - folderId: undefined, - }; - - beforeEach(() => { - jest.spyOn(component, "newItemNavigate"); - }); - - it("navigates to new login", async () => { - await component.newItemNavigate(CipherType.Login); - - expect(navigate).toHaveBeenCalledWith(["/add-cipher"], { - queryParams: { - type: CipherType.Login.toString(), - name: "example.com", - uri: "https://example.com", - ...emptyParams, - }, - }); - }); - - it("navigates to new card", async () => { - await component.newItemNavigate(CipherType.Card); - - expect(navigate).toHaveBeenCalledWith(["/add-cipher"], { - queryParams: { type: CipherType.Card.toString(), ...emptyParams }, - }); - }); - - it("navigates to new identity", async () => { - await component.newItemNavigate(CipherType.Identity); - - expect(navigate).toHaveBeenCalledWith(["/add-cipher"], { - queryParams: { type: CipherType.Identity.toString(), ...emptyParams }, - }); - }); - - it("navigates to new note", async () => { - await component.newItemNavigate(CipherType.SecureNote); - - expect(navigate).toHaveBeenCalledWith(["/add-cipher"], { - queryParams: { type: CipherType.SecureNote.toString(), ...emptyParams }, - }); - }); - - it("includes initial values", async () => { + describe("buildQueryParams", () => { + it("should build query params for a Login cipher when not popped out", async () => { + await component.ngOnInit(); component.initialValues = { folderId: "222-333-444", organizationId: "444-555-666", collectionId: "777-888-999", } as NewItemInitialValues; - await component.newItemNavigate(CipherType.Login); + jest.spyOn(BrowserPopupUtils, "inPopout").mockReturnValue(false); + jest.spyOn(Utils, "getHostname").mockReturnValue("example.com"); - expect(navigate).toHaveBeenCalledWith(["/add-cipher"], { - queryParams: { - type: CipherType.Login.toString(), - folderId: "222-333-444", - organizationId: "444-555-666", - collectionId: "777-888-999", - uri: "https://example.com", - name: "example.com", - }, + const params = component.buildQueryParams(CipherType.Login); + + expect(params).toEqual({ + type: CipherType.Login.toString(), + collectionId: "777-888-999", + organizationId: "444-555-666", + folderId: "222-333-444", + uri: "https://example.com", + name: "example.com", }); }); - it("does not include name or uri when the extension is popped out", async () => { + it("should build query params for a Login cipher when popped out", () => { + component.initialValues = { + collectionId: "777-888-999", + } as NewItemInitialValues; + jest.spyOn(BrowserPopupUtils, "inPopout").mockReturnValue(true); + const params = component.buildQueryParams(CipherType.Login); + + expect(params).toEqual({ + type: CipherType.Login.toString(), + collectionId: "777-888-999", + }); + }); + + it("should build query params for a secure note", () => { component.initialValues = { - folderId: "222-333-444", - organizationId: "444-555-666", collectionId: "777-888-999", } as NewItemInitialValues; - await component.newItemNavigate(CipherType.Login); + const params = component.buildQueryParams(CipherType.SecureNote); - expect(navigate).toHaveBeenCalledWith(["/add-cipher"], { - queryParams: { - type: CipherType.Login.toString(), - folderId: "222-333-444", - organizationId: "444-555-666", - collectionId: "777-888-999", - }, + expect(params).toEqual({ + type: CipherType.SecureNote.toString(), + collectionId: "777-888-999", + }); + }); + + it("should build query params for an Identity", () => { + component.initialValues = { + collectionId: "777-888-999", + } as NewItemInitialValues; + + const params = component.buildQueryParams(CipherType.Identity); + + expect(params).toEqual({ + type: CipherType.Identity.toString(), + collectionId: "777-888-999", + }); + }); + + it("should build query params for a Card", () => { + component.initialValues = { + collectionId: "777-888-999", + } as NewItemInitialValues; + + const params = component.buildQueryParams(CipherType.Card); + + expect(params).toEqual({ + type: CipherType.Card.toString(), + collectionId: "777-888-999", + }); + }); + + it("should build query params for a SshKey", () => { + component.initialValues = { + collectionId: "777-888-999", + } as NewItemInitialValues; + + const params = component.buildQueryParams(CipherType.SshKey); + + expect(params).toEqual({ + type: CipherType.SshKey.toString(), + collectionId: "777-888-999", }); }); }); diff --git a/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.ts index a1d5cbd332d..e2062101e1b 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.ts @@ -1,6 +1,6 @@ import { CommonModule } from "@angular/common"; -import { Component, Input } from "@angular/core"; -import { Router, RouterLink } from "@angular/router"; +import { Component, Input, OnInit } from "@angular/core"; +import { RouterLink } from "@angular/router"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { Utils } from "@bitwarden/common/platform/misc/utils"; @@ -25,31 +25,31 @@ export interface NewItemInitialValues { standalone: true, imports: [NoItemsModule, JslibModule, CommonModule, ButtonModule, RouterLink, MenuModule], }) -export class NewItemDropdownV2Component { +export class NewItemDropdownV2Component implements OnInit { cipherType = CipherType; - + private tab?: chrome.tabs.Tab; /** * Optional initial values to pass to the add cipher form */ @Input() initialValues: NewItemInitialValues; - constructor( - private router: Router, - private dialogService: DialogService, - ) {} + constructor(private dialogService: DialogService) {} - private async buildQueryParams(type: CipherType): Promise { - const tab = await BrowserApi.getTabFromCurrentWindow(); + async ngOnInit() { + this.tab = await BrowserApi.getTabFromCurrentWindow(); + } + + buildQueryParams(type: CipherType): AddEditQueryParams { const poppedOut = BrowserPopupUtils.inPopout(window); const loginDetails: { uri?: string; name?: string } = {}; // When a Login Cipher is created and the extension is not popped out, // pass along the uri and name - if (!poppedOut && type === CipherType.Login && tab) { - loginDetails.uri = tab.url; - loginDetails.name = Utils.getHostname(tab.url); + if (!poppedOut && type === CipherType.Login && this.tab) { + loginDetails.uri = this.tab.url; + loginDetails.name = Utils.getHostname(this.tab.url); } return { @@ -61,10 +61,6 @@ export class NewItemDropdownV2Component { }; } - async newItemNavigate(type: CipherType) { - await this.router.navigate(["/add-cipher"], { queryParams: await this.buildQueryParams(type) }); - } - openFolderDialog() { this.dialogService.open(AddEditFolderDialogComponent); } 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 75334b68ef9..9a65f7d98c5 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 @@ -3,11 +3,19 @@ {{ (hideIcon ? "createSend" : "new") | i18n }} - + {{ "sendTypeText" | i18n }} - + {{ "sendTypeFile" | i18n }} - + diff --git a/apps/web/src/app/tools/credential-generator/credential-generator.component.ts b/apps/web/src/app/tools/credential-generator/credential-generator.component.ts index f252796d062..8d7b56a09ad 100644 --- a/apps/web/src/app/tools/credential-generator/credential-generator.component.ts +++ b/apps/web/src/app/tools/credential-generator/credential-generator.component.ts @@ -1,6 +1,6 @@ import { Component } from "@angular/core"; -import { ButtonModule, DialogService, ItemModule, LinkModule } from "@bitwarden/components"; +import { ButtonModule, DialogService, LinkModule } from "@bitwarden/components"; import { CredentialGeneratorHistoryDialogComponent, GeneratorModule, @@ -13,7 +13,7 @@ import { SharedModule } from "../../shared"; standalone: true, selector: "credential-generator", templateUrl: "credential-generator.component.html", - imports: [SharedModule, HeaderModule, GeneratorModule, ItemModule, ButtonModule, LinkModule], + imports: [SharedModule, HeaderModule, GeneratorModule, ButtonModule, LinkModule], }) export class CredentialGeneratorComponent { constructor(private dialogService: DialogService) {} From 194aa943028e8795798ebf864958c3e03fb3850a Mon Sep 17 00:00:00 2001 From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Date: Tue, 3 Dec 2024 13:46:17 +0100 Subject: [PATCH 05/46] [PM-13202][Defect] MSP name with an apostrophe displaying dummy character in Delete provider screen (#11488) * Resolve the msp name with apostrophe * qParams.name exists and is a string before sanitization --- .../verify-recover-delete-provider.component.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/verify-recover-delete-provider.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/verify-recover-delete-provider.component.ts index a4461b3e11a..68264593b8e 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/verify-recover-delete-provider.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/verify-recover-delete-provider.component.ts @@ -1,4 +1,5 @@ -import { Component, OnInit } from "@angular/core"; +import { Component, OnInit, SecurityContext } from "@angular/core"; +import { DomSanitizer } from "@angular/platform-browser"; import { ActivatedRoute, Router } from "@angular/router"; import { firstValueFrom } from "rxjs"; @@ -24,6 +25,7 @@ export class VerifyRecoverDeleteProviderComponent implements OnInit { private i18nService: I18nService, private route: ActivatedRoute, private toastService: ToastService, + private sanitizer: DomSanitizer, ) {} async ngOnInit() { @@ -31,7 +33,10 @@ export class VerifyRecoverDeleteProviderComponent implements OnInit { if (qParams.providerId != null && qParams.token != null && qParams.name != null) { this.providerId = qParams.providerId; this.token = qParams.token; - this.name = qParams.name; + this.name = + qParams.name && typeof qParams.name === "string" + ? this.sanitizer.sanitize(SecurityContext.HTML, qParams.name) || "" + : ""; } else { await this.router.navigate(["/"]); } From 9c03cffe30cf48e48605a6b025b864b0f711eb5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rui=20Tom=C3=A9?= <108268980+r-tome@users.noreply.github.com> Date: Tue, 3 Dec 2024 12:58:26 +0000 Subject: [PATCH 06/46] [PM-15156] Copy update on Organization member Delete modals (#12182) * Rename 'Remove users' to 'Remove members' in bulk remove dialog * Update warning messages for bulk delete dialog and single member deletion --- .../bulk/bulk-delete-dialog.component.html | 2 +- .../bulk/bulk-remove-dialog.component.html | 4 ++-- .../member-dialog/member-dialog.component.ts | 5 ++++- .../members/members.component.ts | 5 ++++- apps/web/src/locales/en/messages.json | 19 ++++++++++++++++--- 5 files changed, 27 insertions(+), 8 deletions(-) diff --git a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-delete-dialog.component.html b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-delete-dialog.component.html index 9a4ce89671e..bb5294ebf02 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-delete-dialog.component.html +++ b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-delete-dialog.component.html @@ -8,7 +8,7 @@ -

{{ "deleteOrganizationUserWarning" | i18n }}

+

{{ "deleteManyOrganizationUsersWarningDesc" | i18n }}

diff --git a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-remove-dialog.component.html b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-remove-dialog.component.html index 8b921d69814..8727148f4ff 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-remove-dialog.component.html +++ b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-remove-dialog.component.html @@ -1,4 +1,4 @@ - + {{ "noSelectedUsersApplicable" | i18n }} @@ -79,7 +79,7 @@ [disabled]="loading" [bitAction]="submit" > - {{ "removeUsers" | i18n }} + {{ "removeMembers" | i18n }}
- diff --git a/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.ts b/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.ts index e2178e7c02c..a6f04ea6862 100644 --- a/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.ts +++ b/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.ts @@ -9,7 +9,6 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions"; import { PaymentMethodType } from "@bitwarden/common/billing/enums"; -import { ExpandedTaxInfoUpdateRequest } from "@bitwarden/common/billing/models/request/expanded-tax-info-update.request"; import { VerifyBankAccountRequest } from "@bitwarden/common/billing/models/request/verify-bank-account.request"; import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.response"; import { PaymentSourceResponse } from "@bitwarden/common/billing/models/response/payment-source.response"; @@ -181,32 +180,6 @@ export class OrganizationPaymentMethodComponent implements OnDestroy { } }; - protected updateTaxInformation = async (): Promise => { - this.taxInfoComponent.taxFormGroup.updateValueAndValidity(); - this.taxInfoComponent.taxFormGroup.markAllAsTouched(); - - if (this.taxInfoComponent.taxFormGroup.invalid) { - return; - } - - const request = new ExpandedTaxInfoUpdateRequest(); - request.country = this.taxInfoComponent.country; - request.postalCode = this.taxInfoComponent.postalCode; - request.taxId = this.taxInfoComponent.taxId; - request.line1 = this.taxInfoComponent.line1; - request.line2 = this.taxInfoComponent.line2; - request.city = this.taxInfoComponent.city; - request.state = this.taxInfoComponent.state; - - await this.billingApiService.updateOrganizationTaxInformation(this.organizationId, request); - - this.toastService.showToast({ - variant: "success", - title: null, - message: this.i18nService.t("taxInfoUpdated"), - }); - }; - protected verifyBankAccount = async (request: VerifyBankAccountRequest): Promise => { await this.billingApiService.verifyOrganizationBankAccount(this.organizationId, request); this.toastService.showToast({ diff --git a/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog-v2.component.html b/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog-v2.component.html index e41d3d961cd..1767fb485d0 100644 --- a/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog-v2.component.html +++ b/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog-v2.component.html @@ -5,7 +5,11 @@ [showBankAccount]="!!organizationId" [initialPaymentMethod]="initialPaymentMethod" > - + - - diff --git a/apps/web/src/app/billing/shared/payment-method.component.ts b/apps/web/src/app/billing/shared/payment-method.component.ts index 98e6efcd8bd..3159cfa2902 100644 --- a/apps/web/src/app/billing/shared/payment-method.component.ts +++ b/apps/web/src/app/billing/shared/payment-method.component.ts @@ -1,5 +1,5 @@ import { Location } from "@angular/common"; -import { Component, OnDestroy, OnInit, ViewChild } from "@angular/core"; +import { Component, OnDestroy, OnInit } from "@angular/core"; import { FormBuilder, FormControl, Validators } from "@angular/forms"; import { ActivatedRoute, Router } from "@angular/router"; import { lastValueFrom } from "rxjs"; @@ -14,7 +14,6 @@ import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/mode import { SubscriptionResponse } from "@bitwarden/common/billing/models/response/subscription.response"; import { VerifyBankRequest } from "@bitwarden/common/models/request/verify-bank.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 { SyncService } from "@bitwarden/common/platform/sync"; import { DialogService, ToastService } from "@bitwarden/components"; @@ -27,15 +26,12 @@ import { AdjustPaymentDialogResult, openAdjustPaymentDialog, } from "./adjust-payment-dialog/adjust-payment-dialog.component"; -import { TaxInfoComponent } from "./tax-info.component"; @Component({ templateUrl: "payment-method.component.html", }) // eslint-disable-next-line rxjs-angular/prefer-takeuntil export class PaymentMethodComponent implements OnInit, OnDestroy { - @ViewChild(TaxInfoComponent) taxInfo: TaxInfoComponent; - loading = false; firstLoaded = false; billing: BillingPaymentResponse; @@ -59,7 +55,6 @@ export class PaymentMethodComponent implements OnInit, OnDestroy { ]), }); - taxForm = this.formBuilder.group({}); launchPaymentModalAutomatically = false; protected freeTrialData: FreeTrial; @@ -70,7 +65,6 @@ export class PaymentMethodComponent implements OnInit, OnDestroy { protected platformUtilsService: PlatformUtilsService, private router: Router, private location: Location, - private logService: LogService, private route: ActivatedRoute, private formBuilder: FormBuilder, private dialogService: DialogService, @@ -196,15 +190,6 @@ export class PaymentMethodComponent implements OnInit, OnDestroy { await this.load(); }; - submitTaxInfo = async () => { - await this.taxInfo.submitTaxInfo(); - this.toastService.showToast({ - variant: "success", - title: null, - message: this.i18nService.t("taxInfoUpdated"), - }); - }; - determineOrgsWithUpcomingPaymentIssues() { this.freeTrialData = this.trialFlowService.checkForOrgsWithUpcomingPaymentIssues( this.organization, @@ -229,10 +214,6 @@ export class PaymentMethodComponent implements OnInit, OnDestroy { return this.organizationId != null; } - get headerClass() { - return this.forOrganization ? ["page-header"] : ["tabbed-header"]; - } - get paymentSourceClasses() { if (this.paymentSource == null) { return []; diff --git a/apps/web/src/app/billing/shared/tax-info.component.html b/apps/web/src/app/billing/shared/tax-info.component.html index 82d5104a53a..3955c0db816 100644 --- a/apps/web/src/app/billing/shared/tax-info.component.html +++ b/apps/web/src/app/billing/shared/tax-info.component.html @@ -13,51 +13,41 @@ -
+
{{ "zipPostalCode" | i18n }}
-
- - - {{ "includeVAT" | i18n }} - +
+ + {{ "address1" | i18n }} + +
-
-
-
+
+ + {{ "address2" | i18n }} + + +
+
+ + {{ "cityTown" | i18n }} + + +
+
+ + {{ "stateProvince" | i18n }} + + +
+
{{ "taxIdNumber" | i18n }}
-
-
- - {{ "address1" | i18n }} - - -
-
- - {{ "address2" | i18n }} - - -
-
- - {{ "cityTown" | i18n }} - - -
-
- - {{ "stateProvince" | i18n }} - - -
-
diff --git a/apps/web/src/app/billing/shared/tax-info.component.ts b/apps/web/src/app/billing/shared/tax-info.component.ts index 2cd8f7dc366..48452528053 100644 --- a/apps/web/src/app/billing/shared/tax-info.component.ts +++ b/apps/web/src/app/billing/shared/tax-info.component.ts @@ -1,29 +1,18 @@ -import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core"; +import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core"; import { FormControl, FormGroup, Validators } from "@angular/forms"; import { ActivatedRoute } from "@angular/router"; import { Subject, takeUntil } from "rxjs"; +import { debounceTime } from "rxjs/operators"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; +import { TaxServiceAbstraction } from "@bitwarden/common/billing/abstractions/tax.service.abstraction"; +import { CountryListItem } from "@bitwarden/common/billing/models/domain"; import { ExpandedTaxInfoUpdateRequest } from "@bitwarden/common/billing/models/request/expanded-tax-info-update.request"; -import { TaxInfoUpdateRequest } from "@bitwarden/common/billing/models/request/tax-info-update.request"; -import { TaxInfoResponse } from "@bitwarden/common/billing/models/response/tax-info.response"; -import { TaxRateResponse } from "@bitwarden/common/billing/models/response/tax-rate.response"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { SharedModule } from "../../shared"; -type TaxInfoView = Omit & { - includeTaxId: boolean; - [key: string]: unknown; -}; - -type CountryList = { - name: string; - value: string; - disabled: boolean; -}; - @Component({ selector: "app-tax-info", templateUrl: "tax-info.component.html", @@ -31,359 +20,64 @@ type CountryList = { imports: [SharedModule], }) // eslint-disable-next-line rxjs-angular/prefer-takeuntil -export class TaxInfoComponent implements OnInit { - @Input() trialFlow = false; - @Output() onCountryChanged = new EventEmitter(); +export class TaxInfoComponent implements OnInit, OnDestroy { private destroy$ = new Subject(); + @Input() trialFlow = false; + @Output() countryChanged = new EventEmitter(); + @Output() taxInformationChanged: EventEmitter = new EventEmitter(); + taxFormGroup = new FormGroup({ - country: new FormControl(null, [Validators.required]), - postalCode: new FormControl(null), - includeTaxId: new FormControl(null), - taxId: new FormControl(null), - line1: new FormControl(null), - line2: new FormControl(null), - city: new FormControl(null), - state: new FormControl(null), + country: new FormControl(null, [Validators.required]), + postalCode: new FormControl(null, [Validators.required]), + taxId: new FormControl(null), + line1: new FormControl(null), + line2: new FormControl(null), + city: new FormControl(null), + state: new FormControl(null), }); + protected isTaxSupported: boolean; + loading = true; organizationId: string; providerId: string; - taxInfo: TaxInfoView = { - taxId: null, - line1: null, - line2: null, - city: null, - state: null, - postalCode: null, - country: "US", - includeTaxId: false, - }; - countryList: CountryList[] = [ - { name: "-- Select --", value: "", disabled: false }, - { name: "United States", value: "US", disabled: false }, - { name: "China", value: "CN", disabled: false }, - { name: "France", value: "FR", disabled: false }, - { name: "Germany", value: "DE", disabled: false }, - { name: "Canada", value: "CA", disabled: false }, - { name: "United Kingdom", value: "GB", disabled: false }, - { name: "Australia", value: "AU", disabled: false }, - { name: "India", value: "IN", disabled: false }, - { name: "", value: "-", disabled: true }, - { name: "Afghanistan", value: "AF", disabled: false }, - { name: "Åland Islands", value: "AX", disabled: false }, - { name: "Albania", value: "AL", disabled: false }, - { name: "Algeria", value: "DZ", disabled: false }, - { name: "American Samoa", value: "AS", disabled: false }, - { name: "Andorra", value: "AD", disabled: false }, - { name: "Angola", value: "AO", disabled: false }, - { name: "Anguilla", value: "AI", disabled: false }, - { name: "Antarctica", value: "AQ", disabled: false }, - { name: "Antigua and Barbuda", value: "AG", disabled: false }, - { name: "Argentina", value: "AR", disabled: false }, - { name: "Armenia", value: "AM", disabled: false }, - { name: "Aruba", value: "AW", disabled: false }, - { name: "Austria", value: "AT", disabled: false }, - { name: "Azerbaijan", value: "AZ", disabled: false }, - { name: "Bahamas", value: "BS", disabled: false }, - { name: "Bahrain", value: "BH", disabled: false }, - { name: "Bangladesh", value: "BD", disabled: false }, - { name: "Barbados", value: "BB", disabled: false }, - { name: "Belarus", value: "BY", disabled: false }, - { name: "Belgium", value: "BE", disabled: false }, - { name: "Belize", value: "BZ", disabled: false }, - { name: "Benin", value: "BJ", disabled: false }, - { name: "Bermuda", value: "BM", disabled: false }, - { name: "Bhutan", value: "BT", disabled: false }, - { name: "Bolivia, Plurinational State of", value: "BO", disabled: false }, - { name: "Bonaire, Sint Eustatius and Saba", value: "BQ", disabled: false }, - { name: "Bosnia and Herzegovina", value: "BA", disabled: false }, - { name: "Botswana", value: "BW", disabled: false }, - { name: "Bouvet Island", value: "BV", disabled: false }, - { name: "Brazil", value: "BR", disabled: false }, - { name: "British Indian Ocean Territory", value: "IO", disabled: false }, - { name: "Brunei Darussalam", value: "BN", disabled: false }, - { name: "Bulgaria", value: "BG", disabled: false }, - { name: "Burkina Faso", value: "BF", disabled: false }, - { name: "Burundi", value: "BI", disabled: false }, - { name: "Cambodia", value: "KH", disabled: false }, - { name: "Cameroon", value: "CM", disabled: false }, - { name: "Cape Verde", value: "CV", disabled: false }, - { name: "Cayman Islands", value: "KY", disabled: false }, - { name: "Central African Republic", value: "CF", disabled: false }, - { name: "Chad", value: "TD", disabled: false }, - { name: "Chile", value: "CL", disabled: false }, - { name: "Christmas Island", value: "CX", disabled: false }, - { name: "Cocos (Keeling) Islands", value: "CC", disabled: false }, - { name: "Colombia", value: "CO", disabled: false }, - { name: "Comoros", value: "KM", disabled: false }, - { name: "Congo", value: "CG", disabled: false }, - { name: "Congo, the Democratic Republic of the", value: "CD", disabled: false }, - { name: "Cook Islands", value: "CK", disabled: false }, - { name: "Costa Rica", value: "CR", disabled: false }, - { name: "Côte d'Ivoire", value: "CI", disabled: false }, - { name: "Croatia", value: "HR", disabled: false }, - { name: "Cuba", value: "CU", disabled: false }, - { name: "Curaçao", value: "CW", disabled: false }, - { name: "Cyprus", value: "CY", disabled: false }, - { name: "Czech Republic", value: "CZ", disabled: false }, - { name: "Denmark", value: "DK", disabled: false }, - { name: "Djibouti", value: "DJ", disabled: false }, - { name: "Dominica", value: "DM", disabled: false }, - { name: "Dominican Republic", value: "DO", disabled: false }, - { name: "Ecuador", value: "EC", disabled: false }, - { name: "Egypt", value: "EG", disabled: false }, - { name: "El Salvador", value: "SV", disabled: false }, - { name: "Equatorial Guinea", value: "GQ", disabled: false }, - { name: "Eritrea", value: "ER", disabled: false }, - { name: "Estonia", value: "EE", disabled: false }, - { name: "Ethiopia", value: "ET", disabled: false }, - { name: "Falkland Islands (Malvinas)", value: "FK", disabled: false }, - { name: "Faroe Islands", value: "FO", disabled: false }, - { name: "Fiji", value: "FJ", disabled: false }, - { name: "Finland", value: "FI", disabled: false }, - { name: "French Guiana", value: "GF", disabled: false }, - { name: "French Polynesia", value: "PF", disabled: false }, - { name: "French Southern Territories", value: "TF", disabled: false }, - { name: "Gabon", value: "GA", disabled: false }, - { name: "Gambia", value: "GM", disabled: false }, - { name: "Georgia", value: "GE", disabled: false }, - { name: "Ghana", value: "GH", disabled: false }, - { name: "Gibraltar", value: "GI", disabled: false }, - { name: "Greece", value: "GR", disabled: false }, - { name: "Greenland", value: "GL", disabled: false }, - { name: "Grenada", value: "GD", disabled: false }, - { name: "Guadeloupe", value: "GP", disabled: false }, - { name: "Guam", value: "GU", disabled: false }, - { name: "Guatemala", value: "GT", disabled: false }, - { name: "Guernsey", value: "GG", disabled: false }, - { name: "Guinea", value: "GN", disabled: false }, - { name: "Guinea-Bissau", value: "GW", disabled: false }, - { name: "Guyana", value: "GY", disabled: false }, - { name: "Haiti", value: "HT", disabled: false }, - { name: "Heard Island and McDonald Islands", value: "HM", disabled: false }, - { name: "Holy See (Vatican City State)", value: "VA", disabled: false }, - { name: "Honduras", value: "HN", disabled: false }, - { name: "Hong Kong", value: "HK", disabled: false }, - { name: "Hungary", value: "HU", disabled: false }, - { name: "Iceland", value: "IS", disabled: false }, - { name: "Indonesia", value: "ID", disabled: false }, - { name: "Iran, Islamic Republic of", value: "IR", disabled: false }, - { name: "Iraq", value: "IQ", disabled: false }, - { name: "Ireland", value: "IE", disabled: false }, - { name: "Isle of Man", value: "IM", disabled: false }, - { name: "Israel", value: "IL", disabled: false }, - { name: "Italy", value: "IT", disabled: false }, - { name: "Jamaica", value: "JM", disabled: false }, - { name: "Japan", value: "JP", disabled: false }, - { name: "Jersey", value: "JE", disabled: false }, - { name: "Jordan", value: "JO", disabled: false }, - { name: "Kazakhstan", value: "KZ", disabled: false }, - { name: "Kenya", value: "KE", disabled: false }, - { name: "Kiribati", value: "KI", disabled: false }, - { name: "Korea, Democratic People's Republic of", value: "KP", disabled: false }, - { name: "Korea, Republic of", value: "KR", disabled: false }, - { name: "Kuwait", value: "KW", disabled: false }, - { name: "Kyrgyzstan", value: "KG", disabled: false }, - { name: "Lao People's Democratic Republic", value: "LA", disabled: false }, - { name: "Latvia", value: "LV", disabled: false }, - { name: "Lebanon", value: "LB", disabled: false }, - { name: "Lesotho", value: "LS", disabled: false }, - { name: "Liberia", value: "LR", disabled: false }, - { name: "Libya", value: "LY", disabled: false }, - { name: "Liechtenstein", value: "LI", disabled: false }, - { name: "Lithuania", value: "LT", disabled: false }, - { name: "Luxembourg", value: "LU", disabled: false }, - { name: "Macao", value: "MO", disabled: false }, - { name: "Macedonia, the former Yugoslav Republic of", value: "MK", disabled: false }, - { name: "Madagascar", value: "MG", disabled: false }, - { name: "Malawi", value: "MW", disabled: false }, - { name: "Malaysia", value: "MY", disabled: false }, - { name: "Maldives", value: "MV", disabled: false }, - { name: "Mali", value: "ML", disabled: false }, - { name: "Malta", value: "MT", disabled: false }, - { name: "Marshall Islands", value: "MH", disabled: false }, - { name: "Martinique", value: "MQ", disabled: false }, - { name: "Mauritania", value: "MR", disabled: false }, - { name: "Mauritius", value: "MU", disabled: false }, - { name: "Mayotte", value: "YT", disabled: false }, - { name: "Mexico", value: "MX", disabled: false }, - { name: "Micronesia, Federated States of", value: "FM", disabled: false }, - { name: "Moldova, Republic of", value: "MD", disabled: false }, - { name: "Monaco", value: "MC", disabled: false }, - { name: "Mongolia", value: "MN", disabled: false }, - { name: "Montenegro", value: "ME", disabled: false }, - { name: "Montserrat", value: "MS", disabled: false }, - { name: "Morocco", value: "MA", disabled: false }, - { name: "Mozambique", value: "MZ", disabled: false }, - { name: "Myanmar", value: "MM", disabled: false }, - { name: "Namibia", value: "NA", disabled: false }, - { name: "Nauru", value: "NR", disabled: false }, - { name: "Nepal", value: "NP", disabled: false }, - { name: "Netherlands", value: "NL", disabled: false }, - { name: "New Caledonia", value: "NC", disabled: false }, - { name: "New Zealand", value: "NZ", disabled: false }, - { name: "Nicaragua", value: "NI", disabled: false }, - { name: "Niger", value: "NE", disabled: false }, - { name: "Nigeria", value: "NG", disabled: false }, - { name: "Niue", value: "NU", disabled: false }, - { name: "Norfolk Island", value: "NF", disabled: false }, - { name: "Northern Mariana Islands", value: "MP", disabled: false }, - { name: "Norway", value: "NO", disabled: false }, - { name: "Oman", value: "OM", disabled: false }, - { name: "Pakistan", value: "PK", disabled: false }, - { name: "Palau", value: "PW", disabled: false }, - { name: "Palestinian Territory, Occupied", value: "PS", disabled: false }, - { name: "Panama", value: "PA", disabled: false }, - { name: "Papua New Guinea", value: "PG", disabled: false }, - { name: "Paraguay", value: "PY", disabled: false }, - { name: "Peru", value: "PE", disabled: false }, - { name: "Philippines", value: "PH", disabled: false }, - { name: "Pitcairn", value: "PN", disabled: false }, - { name: "Poland", value: "PL", disabled: false }, - { name: "Portugal", value: "PT", disabled: false }, - { name: "Puerto Rico", value: "PR", disabled: false }, - { name: "Qatar", value: "QA", disabled: false }, - { name: "Réunion", value: "RE", disabled: false }, - { name: "Romania", value: "RO", disabled: false }, - { name: "Russian Federation", value: "RU", disabled: false }, - { name: "Rwanda", value: "RW", disabled: false }, - { name: "Saint Barthélemy", value: "BL", disabled: false }, - { name: "Saint Helena, Ascension and Tristan da Cunha", value: "SH", disabled: false }, - { name: "Saint Kitts and Nevis", value: "KN", disabled: false }, - { name: "Saint Lucia", value: "LC", disabled: false }, - { name: "Saint Martin (French part)", value: "MF", disabled: false }, - { name: "Saint Pierre and Miquelon", value: "PM", disabled: false }, - { name: "Saint Vincent and the Grenadines", value: "VC", disabled: false }, - { name: "Samoa", value: "WS", disabled: false }, - { name: "San Marino", value: "SM", disabled: false }, - { name: "Sao Tome and Principe", value: "ST", disabled: false }, - { name: "Saudi Arabia", value: "SA", disabled: false }, - { name: "Senegal", value: "SN", disabled: false }, - { name: "Serbia", value: "RS", disabled: false }, - { name: "Seychelles", value: "SC", disabled: false }, - { name: "Sierra Leone", value: "SL", disabled: false }, - { name: "Singapore", value: "SG", disabled: false }, - { name: "Sint Maarten (Dutch part)", value: "SX", disabled: false }, - { name: "Slovakia", value: "SK", disabled: false }, - { name: "Slovenia", value: "SI", disabled: false }, - { name: "Solomon Islands", value: "SB", disabled: false }, - { name: "Somalia", value: "SO", disabled: false }, - { name: "South Africa", value: "ZA", disabled: false }, - { name: "South Georgia and the South Sandwich Islands", value: "GS", disabled: false }, - { name: "South Sudan", value: "SS", disabled: false }, - { name: "Spain", value: "ES", disabled: false }, - { name: "Sri Lanka", value: "LK", disabled: false }, - { name: "Sudan", value: "SD", disabled: false }, - { name: "Suriname", value: "SR", disabled: false }, - { name: "Svalbard and Jan Mayen", value: "SJ", disabled: false }, - { name: "Swaziland", value: "SZ", disabled: false }, - { name: "Sweden", value: "SE", disabled: false }, - { name: "Switzerland", value: "CH", disabled: false }, - { name: "Syrian Arab Republic", value: "SY", disabled: false }, - { name: "Taiwan", value: "TW", disabled: false }, - { name: "Tajikistan", value: "TJ", disabled: false }, - { name: "Tanzania, United Republic of", value: "TZ", disabled: false }, - { name: "Thailand", value: "TH", disabled: false }, - { name: "Timor-Leste", value: "TL", disabled: false }, - { name: "Togo", value: "TG", disabled: false }, - { name: "Tokelau", value: "TK", disabled: false }, - { name: "Tonga", value: "TO", disabled: false }, - { name: "Trinidad and Tobago", value: "TT", disabled: false }, - { name: "Tunisia", value: "TN", disabled: false }, - { name: "Turkey", value: "TR", disabled: false }, - { name: "Turkmenistan", value: "TM", disabled: false }, - { name: "Turks and Caicos Islands", value: "TC", disabled: false }, - { name: "Tuvalu", value: "TV", disabled: false }, - { name: "Uganda", value: "UG", disabled: false }, - { name: "Ukraine", value: "UA", disabled: false }, - { name: "United Arab Emirates", value: "AE", disabled: false }, - { name: "United States Minor Outlying Islands", value: "UM", disabled: false }, - { name: "Uruguay", value: "UY", disabled: false }, - { name: "Uzbekistan", value: "UZ", disabled: false }, - { name: "Vanuatu", value: "VU", disabled: false }, - { name: "Venezuela, Bolivarian Republic of", value: "VE", disabled: false }, - { name: "Viet Nam", value: "VN", disabled: false }, - { name: "Virgin Islands, British", value: "VG", disabled: false }, - { name: "Virgin Islands, U.S.", value: "VI", disabled: false }, - { name: "Wallis and Futuna", value: "WF", disabled: false }, - { name: "Western Sahara", value: "EH", disabled: false }, - { name: "Yemen", value: "YE", disabled: false }, - { name: "Zambia", value: "ZM", disabled: false }, - { name: "Zimbabwe", value: "ZW", disabled: false }, - ]; - taxRates: TaxRateResponse[]; + countryList: CountryListItem[] = this.taxService.getCountries(); constructor( private apiService: ApiService, private route: ActivatedRoute, private logService: LogService, private organizationApiService: OrganizationApiServiceAbstraction, + private taxService: TaxServiceAbstraction, ) {} get country(): string { - return this.taxFormGroup.get("country").value; - } - - set country(country: string) { - this.taxFormGroup.get("country").setValue(country); + return this.taxFormGroup.controls.country.value; } get postalCode(): string { - return this.taxFormGroup.get("postalCode").value; - } - - set postalCode(postalCode: string) { - this.taxFormGroup.get("postalCode").setValue(postalCode); - } - - get includeTaxId(): boolean { - return this.taxFormGroup.get("includeTaxId").value; - } - - set includeTaxId(includeTaxId: boolean) { - this.taxFormGroup.get("includeTaxId").setValue(includeTaxId); + return this.taxFormGroup.controls.postalCode.value; } get taxId(): string { - return this.taxFormGroup.get("taxId").value; - } - - set taxId(taxId: string) { - this.taxFormGroup.get("taxId").setValue(taxId); + return this.taxFormGroup.controls.taxId.value; } get line1(): string { - return this.taxFormGroup.get("line1").value; - } - - set line1(line1: string) { - this.taxFormGroup.get("line1").setValue(line1); + return this.taxFormGroup.controls.line1.value; } get line2(): string { - return this.taxFormGroup.get("line2").value; - } - - set line2(line2: string) { - this.taxFormGroup.get("line2").setValue(line2); + return this.taxFormGroup.controls.line2.value; } get city(): string { - return this.taxFormGroup.get("city").value; - } - - set city(city: string) { - this.taxFormGroup.get("city").setValue(city); + return this.taxFormGroup.controls.city.value; } get state(): string { - return this.taxFormGroup.get("state").value; - } - - set state(state: string) { - this.taxFormGroup.get("state").setValue(state); + return this.taxFormGroup.controls.state.value; } async ngOnInit() { @@ -400,22 +94,13 @@ export class TaxInfoComponent implements OnInit { try { const taxInfo = await this.organizationApiService.getTaxInfo(this.organizationId); if (taxInfo) { - this.taxId = taxInfo.taxId; - this.state = taxInfo.state; - this.line1 = taxInfo.line1; - this.line2 = taxInfo.line2; - this.city = taxInfo.city; - this.state = taxInfo.state; - this.postalCode = taxInfo.postalCode; - this.country = taxInfo.country || "US"; - this.includeTaxId = - this.countrySupportsTax(this.country) && - (!!taxInfo.taxId || - !!taxInfo.line1 || - !!taxInfo.line2 || - !!taxInfo.city || - !!taxInfo.state); - this.setTaxInfoObject(); + this.taxFormGroup.controls.taxId.setValue(taxInfo.taxId); + this.taxFormGroup.controls.state.setValue(taxInfo.state); + this.taxFormGroup.controls.line1.setValue(taxInfo.line1); + this.taxFormGroup.controls.line2.setValue(taxInfo.line2); + this.taxFormGroup.controls.city.setValue(taxInfo.city); + this.taxFormGroup.controls.postalCode.setValue(taxInfo.postalCode); + this.taxFormGroup.controls.country.setValue(taxInfo.country); } } catch (e) { this.logService.error(e); @@ -424,119 +109,79 @@ export class TaxInfoComponent implements OnInit { try { const taxInfo = await this.apiService.getTaxInfo(); if (taxInfo) { - this.postalCode = taxInfo.postalCode; - this.country = taxInfo.country || "US"; + this.taxFormGroup.controls.postalCode.setValue(taxInfo.postalCode); + this.taxFormGroup.controls.country.setValue(taxInfo.country); } - this.setTaxInfoObject(); } catch (e) { this.logService.error(e); } } - if (this.country === "US") { - this.taxFormGroup.get("postalCode").setValidators([Validators.required]); - this.taxFormGroup.get("postalCode").updateValueAndValidity(); - } + this.isTaxSupported = await this.taxService.isCountrySupported( + this.taxFormGroup.controls.country.value, + ); - if (this.country !== "US") { - this.onCountryChanged.emit(); - } + this.countryChanged.emit(); }); - this.taxFormGroup - .get("country") - .valueChanges.pipe(takeUntil(this.destroy$)) + this.taxFormGroup.controls.country.valueChanges + .pipe(debounceTime(1000), takeUntil(this.destroy$)) .subscribe((value) => { - if (value === "US") { - this.taxFormGroup.get("postalCode").setValidators([Validators.required]); - } else { - this.taxFormGroup.get("postalCode").clearValidators(); - } - this.taxFormGroup.get("postalCode").updateValueAndValidity(); - this.setTaxInfoObject(); - this.changeCountry(); + this.taxService + .isCountrySupported(this.taxFormGroup.controls.country.value) + .then((isSupported) => { + this.isTaxSupported = isSupported; + }) + .catch(() => { + this.isTaxSupported = false; + }) + .finally(() => { + if (!this.isTaxSupported) { + this.taxFormGroup.controls.taxId.setValue(null); + this.taxFormGroup.controls.line1.setValue(null); + this.taxFormGroup.controls.line2.setValue(null); + this.taxFormGroup.controls.city.setValue(null); + this.taxFormGroup.controls.state.setValue(null); + } + + this.countryChanged.emit(); + }); + this.taxInformationChanged.emit(); }); - try { - const taxRates = await this.apiService.getTaxRates(); - if (taxRates) { - this.taxRates = taxRates.data; - } - } catch (e) { - this.logService.error(e); - } finally { - this.loading = false; - } + this.taxFormGroup.controls.postalCode.valueChanges + .pipe(debounceTime(1000), takeUntil(this.destroy$)) + .subscribe(() => { + this.taxInformationChanged.emit(); + }); + + this.taxFormGroup.controls.taxId.valueChanges + .pipe(debounceTime(1000), takeUntil(this.destroy$)) + .subscribe(() => { + this.taxInformationChanged.emit(); + }); + + this.loading = false; } - get taxRate() { - if (this.taxRates != null) { - const localTaxRate = this.taxRates.find( - (x) => x.country === this.country && x.postalCode === this.postalCode, - ); - return localTaxRate?.rate ?? null; - } - } - - setTaxInfoObject() { - this.taxInfo.country = this.country; - this.taxInfo.postalCode = this.postalCode; - this.taxInfo.includeTaxId = this.includeTaxId; - this.taxInfo.taxId = this.taxId; - this.taxInfo.line1 = this.line1; - this.taxInfo.line2 = this.line2; - this.taxInfo.city = this.city; - this.taxInfo.state = this.state; - } - - get showTaxIdCheckbox() { - return ( - (this.organizationId || this.providerId) && - this.country !== "US" && - this.countrySupportsTax(this.taxInfo.country) - ); - } - - get showTaxIdFields() { - return ( - (this.organizationId || this.providerId) && - this.includeTaxId && - this.countrySupportsTax(this.country) - ); - } - - getTaxInfoRequest(): TaxInfoUpdateRequest { - if (this.organizationId || this.providerId) { - const request = new ExpandedTaxInfoUpdateRequest(); - request.country = this.country; - request.postalCode = this.postalCode; - - if (this.includeTaxId) { - request.taxId = this.taxId; - request.line1 = this.line1; - request.line2 = this.line2; - request.city = this.city; - request.state = this.state; - } else { - request.taxId = null; - request.line1 = null; - request.line2 = null; - request.city = null; - request.state = null; - } - return request; - } else { - const request = new TaxInfoUpdateRequest(); - request.postalCode = this.postalCode; - request.country = this.country; - return request; - } + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); } submitTaxInfo(): Promise { this.taxFormGroup.updateValueAndValidity(); this.taxFormGroup.markAllAsTouched(); - const request = this.getTaxInfoRequest(); + + const request = new ExpandedTaxInfoUpdateRequest(); + request.country = this.country; + request.postalCode = this.postalCode; + request.taxId = this.taxId; + request.line1 = this.line1; + request.line2 = this.line2; + request.city = this.city; + request.state = this.state; + return this.organizationId ? this.organizationApiService.updateTaxInfo( this.organizationId, @@ -544,97 +189,4 @@ export class TaxInfoComponent implements OnInit { ) : this.apiService.putTaxInfo(request); } - - changeCountry() { - if (!this.countrySupportsTax(this.country)) { - this.includeTaxId = false; - this.taxId = null; - this.line1 = null; - this.line2 = null; - this.city = null; - this.state = null; - this.setTaxInfoObject(); - } - this.onCountryChanged.emit(); - } - - countrySupportsTax(countryCode: string) { - return this.taxSupportedCountryCodes.includes(countryCode); - } - - private taxSupportedCountryCodes: string[] = [ - "CN", - "FR", - "DE", - "CA", - "GB", - "AU", - "IN", - "AD", - "AR", - "AT", - "BE", - "BO", - "BR", - "BG", - "CL", - "CO", - "CR", - "HR", - "CY", - "CZ", - "DK", - "DO", - "EC", - "EG", - "SV", - "EE", - "FI", - "GE", - "GR", - "HK", - "HU", - "IS", - "ID", - "IQ", - "IE", - "IL", - "IT", - "JP", - "KE", - "KR", - "LV", - "LI", - "LT", - "LU", - "MY", - "MT", - "MX", - "NL", - "NZ", - "NO", - "PE", - "PH", - "PL", - "PT", - "RO", - "RU", - "SA", - "RS", - "SG", - "SK", - "SI", - "ZA", - "ES", - "SE", - "CH", - "TW", - "TH", - "TR", - "UA", - "AE", - "UY", - "VE", - "VN", - ]; } diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index cab0e703a7d..25172f7c779 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -9224,6 +9224,18 @@ "updatedTaxInformation": { "message": "Updated tax information" }, + "billingInvalidTaxIdError": { + "message": "Invalid tax ID, if you believe this is an error please contact support." + }, + "billingTaxIdTypeInferenceError": { + "message": "We were unable to validate your tax ID, if you believe this is an error please contact support." + }, + "billingPreviewInvalidTaxIdError": { + "message": "Invalid tax ID, if you believe this is an error please contact support." + }, + "billingPreviewInvoiceError": { + "message": "An error occurred while previewing the invoice. Please try again later." + }, "unverified": { "message": "Unverified" }, 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 33a20444c2b..74aa468c42e 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.html +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.html @@ -29,7 +29,7 @@
- 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 72d954e8cdc..079174b48b5 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 @@ -109,9 +109,7 @@ export class SetupComponent implements OnInit, OnDestroy { try { this.formGroup.markAllAsTouched(); - const formIsValid = this.formGroup.valid && this.manageTaxInformationComponent.touch(); - - if (!formIsValid) { + if (!this.manageTaxInformationComponent.validate() || !this.formGroup.valid) { return; } @@ -129,14 +127,11 @@ export class SetupComponent implements OnInit, OnDestroy { request.taxInfo.country = taxInformation.country; request.taxInfo.postalCode = taxInformation.postalCode; - - if (taxInformation.includeTaxId) { - request.taxInfo.taxId = taxInformation.taxId; - request.taxInfo.line1 = taxInformation.line1; - request.taxInfo.line2 = taxInformation.line2; - request.taxInfo.city = taxInformation.city; - request.taxInfo.state = taxInformation.state; - } + request.taxInfo.taxId = taxInformation.taxId; + request.taxInfo.line1 = taxInformation.line1; + request.taxInfo.line2 = taxInformation.line2; + request.taxInfo.city = taxInformation.city; + request.taxInfo.state = taxInformation.state; const provider = await this.providerApiService.postProviderSetup(this.providerId, request); diff --git a/libs/angular/src/billing/components/manage-tax-information/manage-tax-information.component.html b/libs/angular/src/billing/components/manage-tax-information/manage-tax-information.component.html index 0b041bd4c06..fbd8af4f8b9 100644 --- a/libs/angular/src/billing/components/manage-tax-information/manage-tax-information.component.html +++ b/libs/angular/src/billing/components/manage-tax-information/manage-tax-information.component.html @@ -1,7 +1,7 @@
- + {{ "country" | i18n }}
- + {{ "zipPostalCode" | i18n }}
-
- - - {{ "includeVAT" | i18n }} - + +
+ + {{ "address1" | i18n }} + + +
+
+ + {{ "address2" | i18n }} + + +
+
+ + {{ "cityTown" | i18n }} + + +
+
+ + {{ "stateProvince" | i18n }} + + +
+
+ + {{ "taxIdNumber" | i18n }} + + +
+
+
+
-
-
- - {{ "taxIdNumber" | i18n }} - - -
-
-
-
- - {{ "address1" | i18n }} - - -
-
- - {{ "address2" | i18n }} - - -
-
- - {{ "cityTown" | i18n }} - - -
-
- - {{ "stateProvince" | i18n }} - - -
-
- diff --git a/libs/angular/src/billing/components/manage-tax-information/manage-tax-information.component.ts b/libs/angular/src/billing/components/manage-tax-information/manage-tax-information.component.ts index e73a6968607..2f79c6f5397 100644 --- a/libs/angular/src/billing/components/manage-tax-information/manage-tax-information.component.ts +++ b/libs/angular/src/billing/components/manage-tax-information/manage-tax-information.component.ts @@ -1,14 +1,10 @@ import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core"; import { FormBuilder, Validators } from "@angular/forms"; import { Subject, takeUntil } from "rxjs"; +import { debounceTime } from "rxjs/operators"; -import { TaxInformation } from "@bitwarden/common/billing/models/domain"; - -type Country = { - name: string; - value: string; - disabled: boolean; -}; +import { TaxServiceAbstraction } from "@bitwarden/common/billing/abstractions/tax.service.abstraction"; +import { CountryListItem, TaxInformation } from "@bitwarden/common/billing/models/domain"; @Component({ selector: "app-manage-tax-information", @@ -17,12 +13,22 @@ type Country = { export class ManageTaxInformationComponent implements OnInit, OnDestroy { @Input() startWith: TaxInformation; @Input() onSubmit?: (taxInformation: TaxInformation) => Promise; + + /** + * Emits when the tax information has changed. + */ + @Output() taxInformationChanged = new EventEmitter(); + + /** + * Emits when the tax information has been updated. + */ @Output() taxInformationUpdated = new EventEmitter(); + private taxInformation: TaxInformation; + protected formGroup = this.formBuilder.group({ country: ["", Validators.required], postalCode: ["", Validators.required], - includeTaxId: false, taxId: "", line1: "", line2: "", @@ -30,16 +36,20 @@ export class ManageTaxInformationComponent implements OnInit, OnDestroy { state: "", }); + protected isTaxSupported: boolean; + private destroy$ = new Subject(); - private taxInformation: TaxInformation; + protected readonly countries: CountryListItem[] = this.taxService.getCountries(); - constructor(private formBuilder: FormBuilder) {} + constructor( + private formBuilder: FormBuilder, + private taxService: TaxServiceAbstraction, + ) {} - getTaxInformation = (): TaxInformation & { includeTaxId: boolean } => ({ - ...this.taxInformation, - includeTaxId: this.formGroup.value.includeTaxId, - }); + getTaxInformation(): TaxInformation { + return this.taxInformation; + } submit = async () => { this.formGroup.markAllAsTouched(); @@ -50,23 +60,28 @@ export class ManageTaxInformationComponent implements OnInit, OnDestroy { this.taxInformationUpdated.emit(); }; - touch = (): boolean => { + validate = (): boolean => { this.formGroup.markAllAsTouched(); return this.formGroup.valid; }; async ngOnInit() { if (this.startWith) { - this.formGroup.patchValue({ - ...this.startWith, - includeTaxId: - this.countrySupportsTax(this.startWith.country) && - (!!this.startWith.taxId || - !!this.startWith.line1 || - !!this.startWith.line2 || - !!this.startWith.city || - !!this.startWith.state), - }); + this.formGroup.controls.country.setValue(this.startWith.country); + this.formGroup.controls.postalCode.setValue(this.startWith.postalCode); + + this.isTaxSupported = + this.startWith && this.startWith.country + ? await this.taxService.isCountrySupported(this.startWith.country) + : false; + + if (this.isTaxSupported) { + this.formGroup.controls.taxId.setValue(this.startWith.taxId); + this.formGroup.controls.line1.setValue(this.startWith.line1); + this.formGroup.controls.line2.setValue(this.startWith.line2); + this.formGroup.controls.city.setValue(this.startWith.city); + this.formGroup.controls.state.setValue(this.startWith.state); + } } this.formGroup.valueChanges.pipe(takeUntil(this.destroy$)).subscribe((values) => { @@ -80,354 +95,47 @@ export class ManageTaxInformationComponent implements OnInit, OnDestroy { state: values.state, }; }); + + this.formGroup.controls.country.valueChanges + .pipe(debounceTime(1000), takeUntil(this.destroy$)) + .subscribe((country: string) => { + this.taxService + .isCountrySupported(country) + .then((isSupported) => (this.isTaxSupported = isSupported)) + .catch(() => (this.isTaxSupported = false)) + .finally(() => { + if (!this.isTaxSupported) { + this.formGroup.controls.taxId.setValue(null); + this.formGroup.controls.line1.setValue(null); + this.formGroup.controls.line2.setValue(null); + this.formGroup.controls.city.setValue(null); + this.formGroup.controls.state.setValue(null); + } + if (this.taxInformationChanged) { + this.taxInformationChanged.emit(this.taxInformation); + } + }); + }); + + this.formGroup.controls.postalCode.valueChanges + .pipe(debounceTime(1000), takeUntil(this.destroy$)) + .subscribe(() => { + if (this.taxInformationChanged) { + this.taxInformationChanged.emit(this.taxInformation); + } + }); + + this.formGroup.controls.taxId.valueChanges + .pipe(debounceTime(1000), takeUntil(this.destroy$)) + .subscribe(() => { + if (this.taxInformationChanged) { + this.taxInformationChanged.emit(this.taxInformation); + } + }); } ngOnDestroy() { this.destroy$.next(); this.destroy$.complete(); } - - protected countrySupportsTax(countryCode: string) { - return this.taxSupportedCountryCodes.includes(countryCode); - } - - protected get includeTaxIdIsSelected() { - return this.formGroup.value.includeTaxId; - } - - protected get selectionSupportsAdditionalOptions() { - return ( - this.formGroup.value.country !== "US" && this.countrySupportsTax(this.formGroup.value.country) - ); - } - - protected countries: Country[] = [ - { name: "-- Select --", value: "", disabled: false }, - { name: "United States", value: "US", disabled: false }, - { name: "China", value: "CN", disabled: false }, - { name: "France", value: "FR", disabled: false }, - { name: "Germany", value: "DE", disabled: false }, - { name: "Canada", value: "CA", disabled: false }, - { name: "United Kingdom", value: "GB", disabled: false }, - { name: "Australia", value: "AU", disabled: false }, - { name: "India", value: "IN", disabled: false }, - { name: "", value: "-", disabled: true }, - { name: "Afghanistan", value: "AF", disabled: false }, - { name: "Åland Islands", value: "AX", disabled: false }, - { name: "Albania", value: "AL", disabled: false }, - { name: "Algeria", value: "DZ", disabled: false }, - { name: "American Samoa", value: "AS", disabled: false }, - { name: "Andorra", value: "AD", disabled: false }, - { name: "Angola", value: "AO", disabled: false }, - { name: "Anguilla", value: "AI", disabled: false }, - { name: "Antarctica", value: "AQ", disabled: false }, - { name: "Antigua and Barbuda", value: "AG", disabled: false }, - { name: "Argentina", value: "AR", disabled: false }, - { name: "Armenia", value: "AM", disabled: false }, - { name: "Aruba", value: "AW", disabled: false }, - { name: "Austria", value: "AT", disabled: false }, - { name: "Azerbaijan", value: "AZ", disabled: false }, - { name: "Bahamas", value: "BS", disabled: false }, - { name: "Bahrain", value: "BH", disabled: false }, - { name: "Bangladesh", value: "BD", disabled: false }, - { name: "Barbados", value: "BB", disabled: false }, - { name: "Belarus", value: "BY", disabled: false }, - { name: "Belgium", value: "BE", disabled: false }, - { name: "Belize", value: "BZ", disabled: false }, - { name: "Benin", value: "BJ", disabled: false }, - { name: "Bermuda", value: "BM", disabled: false }, - { name: "Bhutan", value: "BT", disabled: false }, - { name: "Bolivia, Plurinational State of", value: "BO", disabled: false }, - { name: "Bonaire, Sint Eustatius and Saba", value: "BQ", disabled: false }, - { name: "Bosnia and Herzegovina", value: "BA", disabled: false }, - { name: "Botswana", value: "BW", disabled: false }, - { name: "Bouvet Island", value: "BV", disabled: false }, - { name: "Brazil", value: "BR", disabled: false }, - { name: "British Indian Ocean Territory", value: "IO", disabled: false }, - { name: "Brunei Darussalam", value: "BN", disabled: false }, - { name: "Bulgaria", value: "BG", disabled: false }, - { name: "Burkina Faso", value: "BF", disabled: false }, - { name: "Burundi", value: "BI", disabled: false }, - { name: "Cambodia", value: "KH", disabled: false }, - { name: "Cameroon", value: "CM", disabled: false }, - { name: "Cape Verde", value: "CV", disabled: false }, - { name: "Cayman Islands", value: "KY", disabled: false }, - { name: "Central African Republic", value: "CF", disabled: false }, - { name: "Chad", value: "TD", disabled: false }, - { name: "Chile", value: "CL", disabled: false }, - { name: "Christmas Island", value: "CX", disabled: false }, - { name: "Cocos (Keeling) Islands", value: "CC", disabled: false }, - { name: "Colombia", value: "CO", disabled: false }, - { name: "Comoros", value: "KM", disabled: false }, - { name: "Congo", value: "CG", disabled: false }, - { name: "Congo, the Democratic Republic of the", value: "CD", disabled: false }, - { name: "Cook Islands", value: "CK", disabled: false }, - { name: "Costa Rica", value: "CR", disabled: false }, - { name: "Côte d'Ivoire", value: "CI", disabled: false }, - { name: "Croatia", value: "HR", disabled: false }, - { name: "Cuba", value: "CU", disabled: false }, - { name: "Curaçao", value: "CW", disabled: false }, - { name: "Cyprus", value: "CY", disabled: false }, - { name: "Czech Republic", value: "CZ", disabled: false }, - { name: "Denmark", value: "DK", disabled: false }, - { name: "Djibouti", value: "DJ", disabled: false }, - { name: "Dominica", value: "DM", disabled: false }, - { name: "Dominican Republic", value: "DO", disabled: false }, - { name: "Ecuador", value: "EC", disabled: false }, - { name: "Egypt", value: "EG", disabled: false }, - { name: "El Salvador", value: "SV", disabled: false }, - { name: "Equatorial Guinea", value: "GQ", disabled: false }, - { name: "Eritrea", value: "ER", disabled: false }, - { name: "Estonia", value: "EE", disabled: false }, - { name: "Ethiopia", value: "ET", disabled: false }, - { name: "Falkland Islands (Malvinas)", value: "FK", disabled: false }, - { name: "Faroe Islands", value: "FO", disabled: false }, - { name: "Fiji", value: "FJ", disabled: false }, - { name: "Finland", value: "FI", disabled: false }, - { name: "French Guiana", value: "GF", disabled: false }, - { name: "French Polynesia", value: "PF", disabled: false }, - { name: "French Southern Territories", value: "TF", disabled: false }, - { name: "Gabon", value: "GA", disabled: false }, - { name: "Gambia", value: "GM", disabled: false }, - { name: "Georgia", value: "GE", disabled: false }, - { name: "Ghana", value: "GH", disabled: false }, - { name: "Gibraltar", value: "GI", disabled: false }, - { name: "Greece", value: "GR", disabled: false }, - { name: "Greenland", value: "GL", disabled: false }, - { name: "Grenada", value: "GD", disabled: false }, - { name: "Guadeloupe", value: "GP", disabled: false }, - { name: "Guam", value: "GU", disabled: false }, - { name: "Guatemala", value: "GT", disabled: false }, - { name: "Guernsey", value: "GG", disabled: false }, - { name: "Guinea", value: "GN", disabled: false }, - { name: "Guinea-Bissau", value: "GW", disabled: false }, - { name: "Guyana", value: "GY", disabled: false }, - { name: "Haiti", value: "HT", disabled: false }, - { name: "Heard Island and McDonald Islands", value: "HM", disabled: false }, - { name: "Holy See (Vatican City State)", value: "VA", disabled: false }, - { name: "Honduras", value: "HN", disabled: false }, - { name: "Hong Kong", value: "HK", disabled: false }, - { name: "Hungary", value: "HU", disabled: false }, - { name: "Iceland", value: "IS", disabled: false }, - { name: "Indonesia", value: "ID", disabled: false }, - { name: "Iran, Islamic Republic of", value: "IR", disabled: false }, - { name: "Iraq", value: "IQ", disabled: false }, - { name: "Ireland", value: "IE", disabled: false }, - { name: "Isle of Man", value: "IM", disabled: false }, - { name: "Israel", value: "IL", disabled: false }, - { name: "Italy", value: "IT", disabled: false }, - { name: "Jamaica", value: "JM", disabled: false }, - { name: "Japan", value: "JP", disabled: false }, - { name: "Jersey", value: "JE", disabled: false }, - { name: "Jordan", value: "JO", disabled: false }, - { name: "Kazakhstan", value: "KZ", disabled: false }, - { name: "Kenya", value: "KE", disabled: false }, - { name: "Kiribati", value: "KI", disabled: false }, - { name: "Korea, Democratic People's Republic of", value: "KP", disabled: false }, - { name: "Korea, Republic of", value: "KR", disabled: false }, - { name: "Kuwait", value: "KW", disabled: false }, - { name: "Kyrgyzstan", value: "KG", disabled: false }, - { name: "Lao People's Democratic Republic", value: "LA", disabled: false }, - { name: "Latvia", value: "LV", disabled: false }, - { name: "Lebanon", value: "LB", disabled: false }, - { name: "Lesotho", value: "LS", disabled: false }, - { name: "Liberia", value: "LR", disabled: false }, - { name: "Libya", value: "LY", disabled: false }, - { name: "Liechtenstein", value: "LI", disabled: false }, - { name: "Lithuania", value: "LT", disabled: false }, - { name: "Luxembourg", value: "LU", disabled: false }, - { name: "Macao", value: "MO", disabled: false }, - { name: "Macedonia, the former Yugoslav Republic of", value: "MK", disabled: false }, - { name: "Madagascar", value: "MG", disabled: false }, - { name: "Malawi", value: "MW", disabled: false }, - { name: "Malaysia", value: "MY", disabled: false }, - { name: "Maldives", value: "MV", disabled: false }, - { name: "Mali", value: "ML", disabled: false }, - { name: "Malta", value: "MT", disabled: false }, - { name: "Marshall Islands", value: "MH", disabled: false }, - { name: "Martinique", value: "MQ", disabled: false }, - { name: "Mauritania", value: "MR", disabled: false }, - { name: "Mauritius", value: "MU", disabled: false }, - { name: "Mayotte", value: "YT", disabled: false }, - { name: "Mexico", value: "MX", disabled: false }, - { name: "Micronesia, Federated States of", value: "FM", disabled: false }, - { name: "Moldova, Republic of", value: "MD", disabled: false }, - { name: "Monaco", value: "MC", disabled: false }, - { name: "Mongolia", value: "MN", disabled: false }, - { name: "Montenegro", value: "ME", disabled: false }, - { name: "Montserrat", value: "MS", disabled: false }, - { name: "Morocco", value: "MA", disabled: false }, - { name: "Mozambique", value: "MZ", disabled: false }, - { name: "Myanmar", value: "MM", disabled: false }, - { name: "Namibia", value: "NA", disabled: false }, - { name: "Nauru", value: "NR", disabled: false }, - { name: "Nepal", value: "NP", disabled: false }, - { name: "Netherlands", value: "NL", disabled: false }, - { name: "New Caledonia", value: "NC", disabled: false }, - { name: "New Zealand", value: "NZ", disabled: false }, - { name: "Nicaragua", value: "NI", disabled: false }, - { name: "Niger", value: "NE", disabled: false }, - { name: "Nigeria", value: "NG", disabled: false }, - { name: "Niue", value: "NU", disabled: false }, - { name: "Norfolk Island", value: "NF", disabled: false }, - { name: "Northern Mariana Islands", value: "MP", disabled: false }, - { name: "Norway", value: "NO", disabled: false }, - { name: "Oman", value: "OM", disabled: false }, - { name: "Pakistan", value: "PK", disabled: false }, - { name: "Palau", value: "PW", disabled: false }, - { name: "Palestinian Territory, Occupied", value: "PS", disabled: false }, - { name: "Panama", value: "PA", disabled: false }, - { name: "Papua New Guinea", value: "PG", disabled: false }, - { name: "Paraguay", value: "PY", disabled: false }, - { name: "Peru", value: "PE", disabled: false }, - { name: "Philippines", value: "PH", disabled: false }, - { name: "Pitcairn", value: "PN", disabled: false }, - { name: "Poland", value: "PL", disabled: false }, - { name: "Portugal", value: "PT", disabled: false }, - { name: "Puerto Rico", value: "PR", disabled: false }, - { name: "Qatar", value: "QA", disabled: false }, - { name: "Réunion", value: "RE", disabled: false }, - { name: "Romania", value: "RO", disabled: false }, - { name: "Russian Federation", value: "RU", disabled: false }, - { name: "Rwanda", value: "RW", disabled: false }, - { name: "Saint Barthélemy", value: "BL", disabled: false }, - { name: "Saint Helena, Ascension and Tristan da Cunha", value: "SH", disabled: false }, - { name: "Saint Kitts and Nevis", value: "KN", disabled: false }, - { name: "Saint Lucia", value: "LC", disabled: false }, - { name: "Saint Martin (French part)", value: "MF", disabled: false }, - { name: "Saint Pierre and Miquelon", value: "PM", disabled: false }, - { name: "Saint Vincent and the Grenadines", value: "VC", disabled: false }, - { name: "Samoa", value: "WS", disabled: false }, - { name: "San Marino", value: "SM", disabled: false }, - { name: "Sao Tome and Principe", value: "ST", disabled: false }, - { name: "Saudi Arabia", value: "SA", disabled: false }, - { name: "Senegal", value: "SN", disabled: false }, - { name: "Serbia", value: "RS", disabled: false }, - { name: "Seychelles", value: "SC", disabled: false }, - { name: "Sierra Leone", value: "SL", disabled: false }, - { name: "Singapore", value: "SG", disabled: false }, - { name: "Sint Maarten (Dutch part)", value: "SX", disabled: false }, - { name: "Slovakia", value: "SK", disabled: false }, - { name: "Slovenia", value: "SI", disabled: false }, - { name: "Solomon Islands", value: "SB", disabled: false }, - { name: "Somalia", value: "SO", disabled: false }, - { name: "South Africa", value: "ZA", disabled: false }, - { name: "South Georgia and the South Sandwich Islands", value: "GS", disabled: false }, - { name: "South Sudan", value: "SS", disabled: false }, - { name: "Spain", value: "ES", disabled: false }, - { name: "Sri Lanka", value: "LK", disabled: false }, - { name: "Sudan", value: "SD", disabled: false }, - { name: "Suriname", value: "SR", disabled: false }, - { name: "Svalbard and Jan Mayen", value: "SJ", disabled: false }, - { name: "Swaziland", value: "SZ", disabled: false }, - { name: "Sweden", value: "SE", disabled: false }, - { name: "Switzerland", value: "CH", disabled: false }, - { name: "Syrian Arab Republic", value: "SY", disabled: false }, - { name: "Taiwan", value: "TW", disabled: false }, - { name: "Tajikistan", value: "TJ", disabled: false }, - { name: "Tanzania, United Republic of", value: "TZ", disabled: false }, - { name: "Thailand", value: "TH", disabled: false }, - { name: "Timor-Leste", value: "TL", disabled: false }, - { name: "Togo", value: "TG", disabled: false }, - { name: "Tokelau", value: "TK", disabled: false }, - { name: "Tonga", value: "TO", disabled: false }, - { name: "Trinidad and Tobago", value: "TT", disabled: false }, - { name: "Tunisia", value: "TN", disabled: false }, - { name: "Turkey", value: "TR", disabled: false }, - { name: "Turkmenistan", value: "TM", disabled: false }, - { name: "Turks and Caicos Islands", value: "TC", disabled: false }, - { name: "Tuvalu", value: "TV", disabled: false }, - { name: "Uganda", value: "UG", disabled: false }, - { name: "Ukraine", value: "UA", disabled: false }, - { name: "United Arab Emirates", value: "AE", disabled: false }, - { name: "United States Minor Outlying Islands", value: "UM", disabled: false }, - { name: "Uruguay", value: "UY", disabled: false }, - { name: "Uzbekistan", value: "UZ", disabled: false }, - { name: "Vanuatu", value: "VU", disabled: false }, - { name: "Venezuela, Bolivarian Republic of", value: "VE", disabled: false }, - { name: "Viet Nam", value: "VN", disabled: false }, - { name: "Virgin Islands, British", value: "VG", disabled: false }, - { name: "Virgin Islands, U.S.", value: "VI", disabled: false }, - { name: "Wallis and Futuna", value: "WF", disabled: false }, - { name: "Western Sahara", value: "EH", disabled: false }, - { name: "Yemen", value: "YE", disabled: false }, - { name: "Zambia", value: "ZM", disabled: false }, - { name: "Zimbabwe", value: "ZW", disabled: false }, - ]; - - private taxSupportedCountryCodes: string[] = [ - "CN", - "FR", - "DE", - "CA", - "GB", - "AU", - "IN", - "AD", - "AR", - "AT", - "BE", - "BO", - "BR", - "BG", - "CL", - "CO", - "CR", - "HR", - "CY", - "CZ", - "DK", - "DO", - "EC", - "EG", - "SV", - "EE", - "FI", - "GE", - "GR", - "HK", - "HU", - "IS", - "ID", - "IQ", - "IE", - "IL", - "IT", - "JP", - "KE", - "KR", - "LV", - "LI", - "LT", - "LU", - "MY", - "MT", - "MX", - "NL", - "NZ", - "NO", - "PE", - "PH", - "PL", - "PT", - "RO", - "RU", - "SA", - "RS", - "SG", - "SK", - "SI", - "ZA", - "ES", - "SE", - "CH", - "TW", - "TH", - "TR", - "UA", - "AE", - "UY", - "VE", - "VN", - ]; } diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index a43f1fa07a8..4b876db247d 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -134,11 +134,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 { 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 { OrganizationBillingService } from "@bitwarden/common/billing/services/organization-billing.service"; +import { TaxService } from "@bitwarden/common/billing/services/tax.service"; import { AppIdService as AppIdServiceAbstraction } from "@bitwarden/common/platform/abstractions/app-id.service"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; import { BulkEncryptService } from "@bitwarden/common/platform/abstractions/bulk-encrypt.service"; @@ -1262,6 +1264,11 @@ const safeProviders: SafeProvider[] = [ useClass: BillingApiService, deps: [ApiServiceAbstraction, LogService, ToastService], }), + safeProvider({ + provide: TaxServiceAbstraction, + useClass: TaxService, + deps: [ApiServiceAbstraction], + }), safeProvider({ provide: BillingAccountProfileStateService, useClass: DefaultBillingAccountProfileStateService, diff --git a/libs/common/src/billing/abstractions/tax.service.abstraction.ts b/libs/common/src/billing/abstractions/tax.service.abstraction.ts new file mode 100644 index 00000000000..fea4618bc02 --- /dev/null +++ b/libs/common/src/billing/abstractions/tax.service.abstraction.ts @@ -0,0 +1,18 @@ +import { CountryListItem } from "@bitwarden/common/billing/models/domain"; +import { PreviewIndividualInvoiceRequest } from "@bitwarden/common/billing/models/request/preview-individual-invoice.request"; +import { PreviewOrganizationInvoiceRequest } from "@bitwarden/common/billing/models/request/preview-organization-invoice.request"; +import { PreviewInvoiceResponse } from "@bitwarden/common/billing/models/response/preview-invoice.response"; + +export abstract class TaxServiceAbstraction { + getCountries: () => CountryListItem[]; + + isCountrySupported: (country: string) => Promise; + + previewIndividualInvoice: ( + request: PreviewIndividualInvoiceRequest, + ) => Promise; + + previewOrganizationInvoice: ( + request: PreviewOrganizationInvoiceRequest, + ) => Promise; +} diff --git a/libs/common/src/billing/models/domain/country-list-item.ts b/libs/common/src/billing/models/domain/country-list-item.ts new file mode 100644 index 00000000000..79abb96871c --- /dev/null +++ b/libs/common/src/billing/models/domain/country-list-item.ts @@ -0,0 +1,5 @@ +export type CountryListItem = { + name: string; + value: string; + disabled: boolean; +}; diff --git a/libs/common/src/billing/models/domain/index.ts b/libs/common/src/billing/models/domain/index.ts index 0f53c3e116c..057f6dc4e84 100644 --- a/libs/common/src/billing/models/domain/index.ts +++ b/libs/common/src/billing/models/domain/index.ts @@ -1,2 +1,3 @@ export * from "./bank-account"; +export * from "./country-list-item"; export * from "./tax-information"; diff --git a/libs/common/src/billing/models/request/expanded-tax-info-update.request.ts b/libs/common/src/billing/models/request/expanded-tax-info-update.request.ts index f06795c0805..0cedc5d8e67 100644 --- a/libs/common/src/billing/models/request/expanded-tax-info-update.request.ts +++ b/libs/common/src/billing/models/request/expanded-tax-info-update.request.ts @@ -10,6 +10,10 @@ export class ExpandedTaxInfoUpdateRequest extends TaxInfoUpdateRequest { state: string; static From(taxInformation: TaxInformation): ExpandedTaxInfoUpdateRequest { + if (!taxInformation) { + return null; + } + const request = new ExpandedTaxInfoUpdateRequest(); request.country = taxInformation.country; request.postalCode = taxInformation.postalCode; diff --git a/libs/common/src/billing/models/request/preview-individual-invoice.request.ts b/libs/common/src/billing/models/request/preview-individual-invoice.request.ts new file mode 100644 index 00000000000..67b7b2f3ee6 --- /dev/null +++ b/libs/common/src/billing/models/request/preview-individual-invoice.request.ts @@ -0,0 +1,14 @@ +export class PreviewIndividualInvoiceRequest { + passwordManager: PasswordManager; + taxInformation: TaxInformation; +} + +class PasswordManager { + additionalStorage: number; +} + +class TaxInformation { + postalCode: string; + country: string; + taxId: string; +} 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 new file mode 100644 index 00000000000..2fe2526fdce --- /dev/null +++ b/libs/common/src/billing/models/request/preview-organization-invoice.request.ts @@ -0,0 +1,25 @@ +import { PlanType } from "@bitwarden/common/billing/enums"; + +export class PreviewOrganizationInvoiceRequest { + organizationId?: string; + passwordManager: PasswordManager; + secretsManager?: SecretsManager; + taxInformation: TaxInformation; +} + +class PasswordManager { + plan: PlanType; + seats: number; + additionalStorage: number; +} + +class SecretsManager { + seats: number; + additionalMachineAccounts: number; +} + +class TaxInformation { + postalCode: string; + country: string; + taxId: string; +} diff --git a/libs/common/src/billing/models/response/preview-invoice.response.ts b/libs/common/src/billing/models/response/preview-invoice.response.ts new file mode 100644 index 00000000000..c822a569bb3 --- /dev/null +++ b/libs/common/src/billing/models/response/preview-invoice.response.ts @@ -0,0 +1,16 @@ +import { BaseResponse } from "@bitwarden/common/models/response/base.response"; + +export class PreviewInvoiceResponse extends BaseResponse { + effectiveTaxRate: number; + taxableBaseAmount: number; + taxAmount: number; + totalAmount: number; + + constructor(response: any) { + super(response); + this.effectiveTaxRate = this.getResponseProperty("EffectiveTaxRate"); + this.taxableBaseAmount = this.getResponseProperty("TaxableBaseAmount"); + this.taxAmount = this.getResponseProperty("TaxAmount"); + this.totalAmount = this.getResponseProperty("TotalAmount"); + } +} diff --git a/libs/common/src/billing/models/response/tax-id-types.response.ts b/libs/common/src/billing/models/response/tax-id-types.response.ts new file mode 100644 index 00000000000..0d5cce46c8c --- /dev/null +++ b/libs/common/src/billing/models/response/tax-id-types.response.ts @@ -0,0 +1,28 @@ +import { BaseResponse } from "@bitwarden/common/models/response/base.response"; + +export class TaxIdTypesResponse extends BaseResponse { + taxIdTypes: TaxIdTypeResponse[] = []; + + constructor(response: any) { + super(response); + const taxIdTypes = this.getResponseProperty("TaxIdTypes"); + if (taxIdTypes && taxIdTypes.length) { + this.taxIdTypes = taxIdTypes.map((t: any) => new TaxIdTypeResponse(t)); + } + } +} + +export class TaxIdTypeResponse extends BaseResponse { + code: string; + country: string; + description: string; + example: string; + + constructor(response: any) { + super(response); + this.code = this.getResponseProperty("Code"); + this.country = this.getResponseProperty("Country"); + this.description = this.getResponseProperty("Description"); + this.example = this.getResponseProperty("Example"); + } +} diff --git a/libs/common/src/billing/services/tax.service.ts b/libs/common/src/billing/services/tax.service.ts new file mode 100644 index 00000000000..45e57267ec0 --- /dev/null +++ b/libs/common/src/billing/services/tax.service.ts @@ -0,0 +1,303 @@ +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { TaxServiceAbstraction } from "@bitwarden/common/billing/abstractions/tax.service.abstraction"; +import { CountryListItem } from "@bitwarden/common/billing/models/domain"; +import { PreviewIndividualInvoiceRequest } from "@bitwarden/common/billing/models/request/preview-individual-invoice.request"; +import { PreviewOrganizationInvoiceRequest } from "@bitwarden/common/billing/models/request/preview-organization-invoice.request"; +import { PreviewInvoiceResponse } from "@bitwarden/common/billing/models/response/preview-invoice.response"; + +export class TaxService implements TaxServiceAbstraction { + constructor(private apiService: ApiService) {} + + getCountries(): CountryListItem[] { + return [ + { name: "-- Select --", value: "", disabled: false }, + { name: "United States", value: "US", disabled: false }, + { name: "China", value: "CN", disabled: false }, + { name: "France", value: "FR", disabled: false }, + { name: "Germany", value: "DE", disabled: false }, + { name: "Canada", value: "CA", disabled: false }, + { name: "United Kingdom", value: "GB", disabled: false }, + { name: "Australia", value: "AU", disabled: false }, + { name: "India", value: "IN", disabled: false }, + { name: "", value: "-", disabled: true }, + { name: "Afghanistan", value: "AF", disabled: false }, + { name: "Åland Islands", value: "AX", disabled: false }, + { name: "Albania", value: "AL", disabled: false }, + { name: "Algeria", value: "DZ", disabled: false }, + { name: "American Samoa", value: "AS", disabled: false }, + { name: "Andorra", value: "AD", disabled: false }, + { name: "Angola", value: "AO", disabled: false }, + { name: "Anguilla", value: "AI", disabled: false }, + { name: "Antarctica", value: "AQ", disabled: false }, + { name: "Antigua and Barbuda", value: "AG", disabled: false }, + { name: "Argentina", value: "AR", disabled: false }, + { name: "Armenia", value: "AM", disabled: false }, + { name: "Aruba", value: "AW", disabled: false }, + { name: "Austria", value: "AT", disabled: false }, + { name: "Azerbaijan", value: "AZ", disabled: false }, + { name: "Bahamas", value: "BS", disabled: false }, + { name: "Bahrain", value: "BH", disabled: false }, + { name: "Bangladesh", value: "BD", disabled: false }, + { name: "Barbados", value: "BB", disabled: false }, + { name: "Belarus", value: "BY", disabled: false }, + { name: "Belgium", value: "BE", disabled: false }, + { name: "Belize", value: "BZ", disabled: false }, + { name: "Benin", value: "BJ", disabled: false }, + { name: "Bermuda", value: "BM", disabled: false }, + { name: "Bhutan", value: "BT", disabled: false }, + { name: "Bolivia, Plurinational State of", value: "BO", disabled: false }, + { name: "Bonaire, Sint Eustatius and Saba", value: "BQ", disabled: false }, + { name: "Bosnia and Herzegovina", value: "BA", disabled: false }, + { name: "Botswana", value: "BW", disabled: false }, + { name: "Bouvet Island", value: "BV", disabled: false }, + { name: "Brazil", value: "BR", disabled: false }, + { name: "British Indian Ocean Territory", value: "IO", disabled: false }, + { name: "Brunei Darussalam", value: "BN", disabled: false }, + { name: "Bulgaria", value: "BG", disabled: false }, + { name: "Burkina Faso", value: "BF", disabled: false }, + { name: "Burundi", value: "BI", disabled: false }, + { name: "Cambodia", value: "KH", disabled: false }, + { name: "Cameroon", value: "CM", disabled: false }, + { name: "Cape Verde", value: "CV", disabled: false }, + { name: "Cayman Islands", value: "KY", disabled: false }, + { name: "Central African Republic", value: "CF", disabled: false }, + { name: "Chad", value: "TD", disabled: false }, + { name: "Chile", value: "CL", disabled: false }, + { name: "Christmas Island", value: "CX", disabled: false }, + { name: "Cocos (Keeling) Islands", value: "CC", disabled: false }, + { name: "Colombia", value: "CO", disabled: false }, + { name: "Comoros", value: "KM", disabled: false }, + { name: "Congo", value: "CG", disabled: false }, + { name: "Congo, the Democratic Republic of the", value: "CD", disabled: false }, + { name: "Cook Islands", value: "CK", disabled: false }, + { name: "Costa Rica", value: "CR", disabled: false }, + { name: "Côte d'Ivoire", value: "CI", disabled: false }, + { name: "Croatia", value: "HR", disabled: false }, + { name: "Cuba", value: "CU", disabled: false }, + { name: "Curaçao", value: "CW", disabled: false }, + { name: "Cyprus", value: "CY", disabled: false }, + { name: "Czech Republic", value: "CZ", disabled: false }, + { name: "Denmark", value: "DK", disabled: false }, + { name: "Djibouti", value: "DJ", disabled: false }, + { name: "Dominica", value: "DM", disabled: false }, + { name: "Dominican Republic", value: "DO", disabled: false }, + { name: "Ecuador", value: "EC", disabled: false }, + { name: "Egypt", value: "EG", disabled: false }, + { name: "El Salvador", value: "SV", disabled: false }, + { name: "Equatorial Guinea", value: "GQ", disabled: false }, + { name: "Eritrea", value: "ER", disabled: false }, + { name: "Estonia", value: "EE", disabled: false }, + { name: "Ethiopia", value: "ET", disabled: false }, + { name: "Falkland Islands (Malvinas)", value: "FK", disabled: false }, + { name: "Faroe Islands", value: "FO", disabled: false }, + { name: "Fiji", value: "FJ", disabled: false }, + { name: "Finland", value: "FI", disabled: false }, + { name: "French Guiana", value: "GF", disabled: false }, + { name: "French Polynesia", value: "PF", disabled: false }, + { name: "French Southern Territories", value: "TF", disabled: false }, + { name: "Gabon", value: "GA", disabled: false }, + { name: "Gambia", value: "GM", disabled: false }, + { name: "Georgia", value: "GE", disabled: false }, + { name: "Ghana", value: "GH", disabled: false }, + { name: "Gibraltar", value: "GI", disabled: false }, + { name: "Greece", value: "GR", disabled: false }, + { name: "Greenland", value: "GL", disabled: false }, + { name: "Grenada", value: "GD", disabled: false }, + { name: "Guadeloupe", value: "GP", disabled: false }, + { name: "Guam", value: "GU", disabled: false }, + { name: "Guatemala", value: "GT", disabled: false }, + { name: "Guernsey", value: "GG", disabled: false }, + { name: "Guinea", value: "GN", disabled: false }, + { name: "Guinea-Bissau", value: "GW", disabled: false }, + { name: "Guyana", value: "GY", disabled: false }, + { name: "Haiti", value: "HT", disabled: false }, + { name: "Heard Island and McDonald Islands", value: "HM", disabled: false }, + { name: "Holy See (Vatican City State)", value: "VA", disabled: false }, + { name: "Honduras", value: "HN", disabled: false }, + { name: "Hong Kong", value: "HK", disabled: false }, + { name: "Hungary", value: "HU", disabled: false }, + { name: "Iceland", value: "IS", disabled: false }, + { name: "Indonesia", value: "ID", disabled: false }, + { name: "Iran, Islamic Republic of", value: "IR", disabled: false }, + { name: "Iraq", value: "IQ", disabled: false }, + { name: "Ireland", value: "IE", disabled: false }, + { name: "Isle of Man", value: "IM", disabled: false }, + { name: "Israel", value: "IL", disabled: false }, + { name: "Italy", value: "IT", disabled: false }, + { name: "Jamaica", value: "JM", disabled: false }, + { name: "Japan", value: "JP", disabled: false }, + { name: "Jersey", value: "JE", disabled: false }, + { name: "Jordan", value: "JO", disabled: false }, + { name: "Kazakhstan", value: "KZ", disabled: false }, + { name: "Kenya", value: "KE", disabled: false }, + { name: "Kiribati", value: "KI", disabled: false }, + { name: "Korea, Democratic People's Republic of", value: "KP", disabled: false }, + { name: "Korea, Republic of", value: "KR", disabled: false }, + { name: "Kuwait", value: "KW", disabled: false }, + { name: "Kyrgyzstan", value: "KG", disabled: false }, + { name: "Lao People's Democratic Republic", value: "LA", disabled: false }, + { name: "Latvia", value: "LV", disabled: false }, + { name: "Lebanon", value: "LB", disabled: false }, + { name: "Lesotho", value: "LS", disabled: false }, + { name: "Liberia", value: "LR", disabled: false }, + { name: "Libya", value: "LY", disabled: false }, + { name: "Liechtenstein", value: "LI", disabled: false }, + { name: "Lithuania", value: "LT", disabled: false }, + { name: "Luxembourg", value: "LU", disabled: false }, + { name: "Macao", value: "MO", disabled: false }, + { name: "Macedonia, the former Yugoslav Republic of", value: "MK", disabled: false }, + { name: "Madagascar", value: "MG", disabled: false }, + { name: "Malawi", value: "MW", disabled: false }, + { name: "Malaysia", value: "MY", disabled: false }, + { name: "Maldives", value: "MV", disabled: false }, + { name: "Mali", value: "ML", disabled: false }, + { name: "Malta", value: "MT", disabled: false }, + { name: "Marshall Islands", value: "MH", disabled: false }, + { name: "Martinique", value: "MQ", disabled: false }, + { name: "Mauritania", value: "MR", disabled: false }, + { name: "Mauritius", value: "MU", disabled: false }, + { name: "Mayotte", value: "YT", disabled: false }, + { name: "Mexico", value: "MX", disabled: false }, + { name: "Micronesia, Federated States of", value: "FM", disabled: false }, + { name: "Moldova, Republic of", value: "MD", disabled: false }, + { name: "Monaco", value: "MC", disabled: false }, + { name: "Mongolia", value: "MN", disabled: false }, + { name: "Montenegro", value: "ME", disabled: false }, + { name: "Montserrat", value: "MS", disabled: false }, + { name: "Morocco", value: "MA", disabled: false }, + { name: "Mozambique", value: "MZ", disabled: false }, + { name: "Myanmar", value: "MM", disabled: false }, + { name: "Namibia", value: "NA", disabled: false }, + { name: "Nauru", value: "NR", disabled: false }, + { name: "Nepal", value: "NP", disabled: false }, + { name: "Netherlands", value: "NL", disabled: false }, + { name: "New Caledonia", value: "NC", disabled: false }, + { name: "New Zealand", value: "NZ", disabled: false }, + { name: "Nicaragua", value: "NI", disabled: false }, + { name: "Niger", value: "NE", disabled: false }, + { name: "Nigeria", value: "NG", disabled: false }, + { name: "Niue", value: "NU", disabled: false }, + { name: "Norfolk Island", value: "NF", disabled: false }, + { name: "Northern Mariana Islands", value: "MP", disabled: false }, + { name: "Norway", value: "NO", disabled: false }, + { name: "Oman", value: "OM", disabled: false }, + { name: "Pakistan", value: "PK", disabled: false }, + { name: "Palau", value: "PW", disabled: false }, + { name: "Palestinian Territory, Occupied", value: "PS", disabled: false }, + { name: "Panama", value: "PA", disabled: false }, + { name: "Papua New Guinea", value: "PG", disabled: false }, + { name: "Paraguay", value: "PY", disabled: false }, + { name: "Peru", value: "PE", disabled: false }, + { name: "Philippines", value: "PH", disabled: false }, + { name: "Pitcairn", value: "PN", disabled: false }, + { name: "Poland", value: "PL", disabled: false }, + { name: "Portugal", value: "PT", disabled: false }, + { name: "Puerto Rico", value: "PR", disabled: false }, + { name: "Qatar", value: "QA", disabled: false }, + { name: "Réunion", value: "RE", disabled: false }, + { name: "Romania", value: "RO", disabled: false }, + { name: "Russian Federation", value: "RU", disabled: false }, + { name: "Rwanda", value: "RW", disabled: false }, + { name: "Saint Barthélemy", value: "BL", disabled: false }, + { name: "Saint Helena, Ascension and Tristan da Cunha", value: "SH", disabled: false }, + { name: "Saint Kitts and Nevis", value: "KN", disabled: false }, + { name: "Saint Lucia", value: "LC", disabled: false }, + { name: "Saint Martin (French part)", value: "MF", disabled: false }, + { name: "Saint Pierre and Miquelon", value: "PM", disabled: false }, + { name: "Saint Vincent and the Grenadines", value: "VC", disabled: false }, + { name: "Samoa", value: "WS", disabled: false }, + { name: "San Marino", value: "SM", disabled: false }, + { name: "Sao Tome and Principe", value: "ST", disabled: false }, + { name: "Saudi Arabia", value: "SA", disabled: false }, + { name: "Senegal", value: "SN", disabled: false }, + { name: "Serbia", value: "RS", disabled: false }, + { name: "Seychelles", value: "SC", disabled: false }, + { name: "Sierra Leone", value: "SL", disabled: false }, + { name: "Singapore", value: "SG", disabled: false }, + { name: "Sint Maarten (Dutch part)", value: "SX", disabled: false }, + { name: "Slovakia", value: "SK", disabled: false }, + { name: "Slovenia", value: "SI", disabled: false }, + { name: "Solomon Islands", value: "SB", disabled: false }, + { name: "Somalia", value: "SO", disabled: false }, + { name: "South Africa", value: "ZA", disabled: false }, + { name: "South Georgia and the South Sandwich Islands", value: "GS", disabled: false }, + { name: "South Sudan", value: "SS", disabled: false }, + { name: "Spain", value: "ES", disabled: false }, + { name: "Sri Lanka", value: "LK", disabled: false }, + { name: "Sudan", value: "SD", disabled: false }, + { name: "Suriname", value: "SR", disabled: false }, + { name: "Svalbard and Jan Mayen", value: "SJ", disabled: false }, + { name: "Swaziland", value: "SZ", disabled: false }, + { name: "Sweden", value: "SE", disabled: false }, + { name: "Switzerland", value: "CH", disabled: false }, + { name: "Syrian Arab Republic", value: "SY", disabled: false }, + { name: "Taiwan", value: "TW", disabled: false }, + { name: "Tajikistan", value: "TJ", disabled: false }, + { name: "Tanzania, United Republic of", value: "TZ", disabled: false }, + { name: "Thailand", value: "TH", disabled: false }, + { name: "Timor-Leste", value: "TL", disabled: false }, + { name: "Togo", value: "TG", disabled: false }, + { name: "Tokelau", value: "TK", disabled: false }, + { name: "Tonga", value: "TO", disabled: false }, + { name: "Trinidad and Tobago", value: "TT", disabled: false }, + { name: "Tunisia", value: "TN", disabled: false }, + { name: "Turkey", value: "TR", disabled: false }, + { name: "Turkmenistan", value: "TM", disabled: false }, + { name: "Turks and Caicos Islands", value: "TC", disabled: false }, + { name: "Tuvalu", value: "TV", disabled: false }, + { name: "Uganda", value: "UG", disabled: false }, + { name: "Ukraine", value: "UA", disabled: false }, + { name: "United Arab Emirates", value: "AE", disabled: false }, + { name: "United States Minor Outlying Islands", value: "UM", disabled: false }, + { name: "Uruguay", value: "UY", disabled: false }, + { name: "Uzbekistan", value: "UZ", disabled: false }, + { name: "Vanuatu", value: "VU", disabled: false }, + { name: "Venezuela, Bolivarian Republic of", value: "VE", disabled: false }, + { name: "Viet Nam", value: "VN", disabled: false }, + { name: "Virgin Islands, British", value: "VG", disabled: false }, + { name: "Virgin Islands, U.S.", value: "VI", disabled: false }, + { name: "Wallis and Futuna", value: "WF", disabled: false }, + { name: "Western Sahara", value: "EH", disabled: false }, + { name: "Yemen", value: "YE", disabled: false }, + { name: "Zambia", value: "ZM", disabled: false }, + { name: "Zimbabwe", value: "ZW", disabled: false }, + ]; + } + + async isCountrySupported(country: string): Promise { + const response = await this.apiService.send( + "GET", + "/tax/is-country-supported?country=" + country, + null, + true, + true, + ); + return response; + } + + async previewIndividualInvoice( + request: PreviewIndividualInvoiceRequest, + ): Promise { + const response = await this.apiService.send( + "POST", + "/accounts/billing/preview-invoice", + request, + true, + true, + ); + return new PreviewInvoiceResponse(response); + } + + async previewOrganizationInvoice( + request: PreviewOrganizationInvoiceRequest, + ): Promise { + const response = await this.apiService.send( + "POST", + `/invoices/preview-organization`, + request, + true, + true, + ); + return new PreviewInvoiceResponse(response); + } +} From 864e6759fdb8afd5a96f011a5cc13f60a7701d6b Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Wed, 4 Dec 2024 12:54:55 +0100 Subject: [PATCH 19/46] Switch to rustcrypto argon2 on desktop (#11753) * Switch to rustcrypto argon2 on desktop * Make argon2 use zeroize * Remove argon2 native modules from electron-builder config * Clean rust implementation of argon2 * Update cargo.lock * Update apps/desktop/desktop_native/napi/src/lib.rs Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Add tests * Clean up test * Remove argon2 external from webpack main * Fix build * Fix argon2 module causing a startup crash --------- Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> --- apps/desktop/desktop_native/Cargo.lock | 42 +++++++++++++-- apps/desktop/desktop_native/core/Cargo.toml | 1 + .../desktop_native/core/src/crypto/crypto.rs | 52 ++++++++++++++++++- apps/desktop/desktop_native/core/src/error.rs | 8 +++ apps/desktop/desktop_native/napi/Cargo.toml | 2 +- apps/desktop/desktop_native/napi/index.d.ts | 3 ++ apps/desktop/desktop_native/napi/src/lib.rs | 19 +++++++ apps/desktop/electron-builder.json | 7 +-- .../renderer-crypto-function.service.ts | 7 +++ apps/desktop/src/package-lock.json | 3 +- apps/desktop/src/package.json | 3 +- .../main/main-crypto-function.service.ts | 11 ++-- apps/desktop/src/platform/preload.ts | 4 +- apps/desktop/webpack.main.js | 2 - .../services/node-crypto-function.service.ts | 2 +- 15 files changed, 140 insertions(+), 26 deletions(-) diff --git a/apps/desktop/desktop_native/Cargo.lock b/apps/desktop/desktop_native/Cargo.lock index 16a5a48cd06..5e6a697c6cb 100644 --- a/apps/desktop/desktop_native/Cargo.lock +++ b/apps/desktop/desktop_native/Cargo.lock @@ -83,6 +83,19 @@ dependencies = [ "x11rb", ] +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures", + "password-hash", + "zeroize", +] + [[package]] name = "async-broadcast" version = "0.7.1" @@ -320,6 +333,15 @@ dependencies = [ "tokio-util", ] +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -660,6 +682,7 @@ dependencies = [ "aes", "anyhow", "arboard", + "argon2", "async-stream", "base64", "bitwarden-russh", @@ -1418,9 +1441,9 @@ checksum = "e1c0f5d67ee408a4685b61f5ab7e58605c8ae3f2b4189f0127d804ff13d5560a" [[package]] name = "napi-derive" -version = "2.16.12" +version = "2.16.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17435f7a00bfdab20b0c27d9c56f58f6499e418252253081bfff448099da31d1" +checksum = "7cbe2585d8ac223f7d34f13701434b9d5f4eb9c332cccce8dee57ea18ab8ab0c" dependencies = [ "cfg-if", "convert_case", @@ -1432,9 +1455,9 @@ dependencies = [ [[package]] name = "napi-derive-backend" -version = "1.0.74" +version = "1.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "967c485e00f0bf3b1bdbe510a38a4606919cf1d34d9a37ad41f25a81aa077abe" +checksum = "1639aaa9eeb76e91c6ae66da8ce3e89e921cd3885e99ec85f4abacae72fc91bf" dependencies = [ "convert_case", "once_cell", @@ -1726,6 +1749,17 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core", + "subtle", +] + [[package]] name = "pbkdf2" version = "0.12.2" diff --git a/apps/desktop/desktop_native/core/Cargo.toml b/apps/desktop/desktop_native/core/Cargo.toml index 6abf7fbb0a7..f9b58b7a27d 100644 --- a/apps/desktop/desktop_native/core/Cargo.toml +++ b/apps/desktop/desktop_native/core/Cargo.toml @@ -27,6 +27,7 @@ anyhow = "=1.0.93" arboard = { version = "=3.4.1", default-features = false, features = [ "wayland-data-control", ] } +argon2 = { version = "=0.5.3", features = ["zeroize"] } async-stream = "=0.3.6" base64 = "=0.22.1" byteorder = "=1.5.0" diff --git a/apps/desktop/desktop_native/core/src/crypto/crypto.rs b/apps/desktop/desktop_native/core/src/crypto/crypto.rs index b1fd4426fa6..4427138cb1d 100644 --- a/apps/desktop/desktop_native/core/src/crypto/crypto.rs +++ b/apps/desktop/desktop_native/core/src/crypto/crypto.rs @@ -5,7 +5,7 @@ use aes::cipher::{ BlockEncryptMut, KeyIvInit, }; -use crate::error::{CryptoError, Result}; +use crate::error::{CryptoError, KdfParamError, Result}; use super::CipherString; @@ -37,3 +37,53 @@ pub fn encrypt_aes256( Ok(CipherString::AesCbc256_B64 { iv, data }) } + +pub fn argon2( + secret: &[u8], + salt: &[u8], + iterations: u32, + memory: u32, + parallelism: u32, +) -> Result<[u8; 32]> { + use argon2::*; + + let params = Params::new(memory, iterations, parallelism, Some(32)).map_err(|e| { + KdfParamError::InvalidParams(format!("Argon2 parameters are invalid: {e}",)) + })?; + let argon = Argon2::new(Algorithm::Argon2id, Version::V0x13, params); + + let mut hash = [0u8; 32]; + argon + .hash_password_into(secret, &salt, &mut hash) + .map_err(|e| KdfParamError::InvalidParams(format!("Argon2 hashing failed: {e}",)))?; + + // Argon2 is using some stack memory that is not zeroed. Eventually some function will + // overwrite the stack, but we use this trick to force the used stack to be zeroed. + #[inline(never)] + fn clear_stack() { + std::hint::black_box([0u8; 4096]); + } + clear_stack(); + Ok(hash) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_argon2() { + let test_hash: [u8; 32] = [ + 112, 200, 85, 209, 100, 4, 246, 146, 117, 180, 152, 44, 103, 198, 75, 14, 166, 77, 201, + 22, 62, 178, 87, 224, 95, 209, 253, 68, 166, 209, 47, 218, + ]; + let secret = b"supersecurepassword"; + let salt = b"mail@example.com"; + let iterations = 3; + let memory = 1024 * 64; + let parallelism = 4; + + let hash = argon2(secret, salt, iterations, memory, parallelism).unwrap(); + assert_eq!(hash, test_hash,); + } +} diff --git a/apps/desktop/desktop_native/core/src/error.rs b/apps/desktop/desktop_native/core/src/error.rs index d3104cb6f44..34624ed630e 100644 --- a/apps/desktop/desktop_native/core/src/error.rs +++ b/apps/desktop/desktop_native/core/src/error.rs @@ -9,6 +9,8 @@ pub enum Error { #[error("Cryptography Error, {0}")] Crypto(#[from] CryptoError), + #[error("KDF Parameter Error, {0}")] + KdfParam(#[from] KdfParamError), } #[derive(Debug, Error)] @@ -29,6 +31,12 @@ pub enum CryptoError { KeyDecrypt, } +#[derive(Debug, Error)] +pub enum KdfParamError { + #[error("Invalid KDF parameters: {0}")] + InvalidParams(String), +} + // Ensure that the error messages implement Send and Sync #[cfg(test)] const _: () = { diff --git a/apps/desktop/desktop_native/napi/Cargo.toml b/apps/desktop/desktop_native/napi/Cargo.toml index 644ff8a51d8..6efd662b351 100644 --- a/apps/desktop/desktop_native/napi/Cargo.toml +++ b/apps/desktop/desktop_native/napi/Cargo.toml @@ -19,7 +19,7 @@ hex = "=0.4.3" anyhow = "=1.0.93" desktop_core = { path = "../core" } napi = { version = "=2.16.13", features = ["async"] } -napi-derive = "=2.16.12" +napi-derive = "=2.16.13" tokio = { version = "=1.41.1" } tokio-util = "=0.7.12" tokio-stream = "=0.1.15" diff --git a/apps/desktop/desktop_native/napi/index.d.ts b/apps/desktop/desktop_native/napi/index.d.ts index 956b2d726da..009f29333a5 100644 --- a/apps/desktop/desktop_native/napi/index.d.ts +++ b/apps/desktop/desktop_native/napi/index.d.ts @@ -124,3 +124,6 @@ export declare namespace ipc { send(message: string): number } } +export declare namespace crypto { + export function argon2(secret: Buffer, salt: Buffer, iterations: number, memory: number, parallelism: number): Promise +} diff --git a/apps/desktop/desktop_native/napi/src/lib.rs b/apps/desktop/desktop_native/napi/src/lib.rs index 4c7bc8eaa93..c5ca655bce6 100644 --- a/apps/desktop/desktop_native/napi/src/lib.rs +++ b/apps/desktop/desktop_native/napi/src/lib.rs @@ -528,3 +528,22 @@ pub mod ipc { } } } + +#[napi] +pub mod crypto { + use napi::bindgen_prelude::Buffer; + + #[napi] + pub async fn argon2( + secret: Buffer, + salt: Buffer, + iterations: u32, + memory: u32, + parallelism: u32, + ) -> napi::Result { + desktop_core::crypto::argon2(&secret, &salt, iterations, memory, parallelism) + .map_err(|e| napi::Error::from_reason(e.to_string())) + .map(|v| v.to_vec()) + .map(|v| Buffer::from(v)) + } +} diff --git a/apps/desktop/electron-builder.json b/apps/desktop/electron-builder.json index 9a8bc45ae20..e4649a083a3 100644 --- a/apps/desktop/electron-builder.json +++ b/apps/desktop/electron-builder.json @@ -18,12 +18,7 @@ "**/*", "!**/node_modules/@bitwarden/desktop-napi/**/*", "**/node_modules/@bitwarden/desktop-napi/index.js", - "**/node_modules/@bitwarden/desktop-napi/desktop_napi.${platform}-${arch}*.node", - - "!**/node_modules/argon2/**/*", - "**/node_modules/argon2/argon2.cjs", - "**/node_modules/argon2/package.json", - "**/node_modules/argon2/build/Release/argon2.node" + "**/node_modules/@bitwarden/desktop-napi/desktop_napi.${platform}-${arch}*.node" ], "electronVersion": "32.1.1", "generateUpdatesFilesForAllChannels": true, diff --git a/apps/desktop/src/app/services/renderer-crypto-function.service.ts b/apps/desktop/src/app/services/renderer-crypto-function.service.ts index 604e70b1ebd..ee80dc25933 100644 --- a/apps/desktop/src/app/services/renderer-crypto-function.service.ts +++ b/apps/desktop/src/app/services/renderer-crypto-function.service.ts @@ -19,6 +19,13 @@ export class RendererCryptoFunctionService memory: number, parallelism: number, ): Promise { + if (typeof password === "string") { + password = new TextEncoder().encode(password); + } + if (typeof salt === "string") { + salt = new TextEncoder().encode(salt); + } + return await ipc.platform.crypto.argon2(password, salt, iterations, memory, parallelism); } } diff --git a/apps/desktop/src/package-lock.json b/apps/desktop/src/package-lock.json index 085181e34b2..0300b0b93cc 100644 --- a/apps/desktop/src/package-lock.json +++ b/apps/desktop/src/package-lock.json @@ -9,8 +9,7 @@ "version": "2024.12.0", "license": "GPL-3.0", "dependencies": { - "@bitwarden/desktop-napi": "file:../desktop_native/napi", - "argon2": "0.41.1" + "@bitwarden/desktop-napi": "file:../desktop_native/napi" } }, "../desktop_native/napi": { diff --git a/apps/desktop/src/package.json b/apps/desktop/src/package.json index 0f82609b767..9a3c56cf17c 100644 --- a/apps/desktop/src/package.json +++ b/apps/desktop/src/package.json @@ -12,7 +12,6 @@ "url": "git+https://github.com/bitwarden/clients.git" }, "dependencies": { - "@bitwarden/desktop-napi": "file:../desktop_native/napi", - "argon2": "0.41.1" + "@bitwarden/desktop-napi": "file:../desktop_native/napi" } } diff --git a/apps/desktop/src/platform/main/main-crypto-function.service.ts b/apps/desktop/src/platform/main/main-crypto-function.service.ts index 848e33113e5..2fc3fde1db2 100644 --- a/apps/desktop/src/platform/main/main-crypto-function.service.ts +++ b/apps/desktop/src/platform/main/main-crypto-function.service.ts @@ -1,6 +1,7 @@ import { ipcMain } from "electron"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; +import { crypto } from "@bitwarden/desktop-napi"; import { NodeCryptoFunctionService } from "@bitwarden/node/services/node-crypto-function.service"; export class MainCryptoFunctionService @@ -13,16 +14,16 @@ export class MainCryptoFunctionService async ( event, opts: { - password: string | Uint8Array; - salt: string | Uint8Array; + password: Uint8Array; + salt: Uint8Array; iterations: number; memory: number; parallelism: number; }, ) => { - return await this.argon2( - opts.password, - opts.salt, + return await crypto.argon2( + Buffer.from(opts.password), + Buffer.from(opts.salt), opts.iterations, opts.memory, opts.parallelism, diff --git a/apps/desktop/src/platform/preload.ts b/apps/desktop/src/platform/preload.ts index 519ed684320..30e248d352c 100644 --- a/apps/desktop/src/platform/preload.ts +++ b/apps/desktop/src/platform/preload.ts @@ -99,8 +99,8 @@ const nativeMessaging = { const crypto = { argon2: ( - password: string | Uint8Array, - salt: string | Uint8Array, + password: Uint8Array, + salt: Uint8Array, iterations: number, memory: number, parallelism: number, diff --git a/apps/desktop/webpack.main.js b/apps/desktop/webpack.main.js index d52a947e362..25a68d8c867 100644 --- a/apps/desktop/webpack.main.js +++ b/apps/desktop/webpack.main.js @@ -81,8 +81,6 @@ const main = { externals: { "electron-reload": "commonjs2 electron-reload", "@bitwarden/desktop-napi": "commonjs2 @bitwarden/desktop-napi", - - argon2: "commonjs2 argon2", }, }; diff --git a/libs/node/src/services/node-crypto-function.service.ts b/libs/node/src/services/node-crypto-function.service.ts index c8d676eeaa5..b4126b0813f 100644 --- a/libs/node/src/services/node-crypto-function.service.ts +++ b/libs/node/src/services/node-crypto-function.service.ts @@ -1,6 +1,5 @@ import * as crypto from "crypto"; -import * as argon2 from "argon2"; import * as forge from "node-forge"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; @@ -40,6 +39,7 @@ export class NodeCryptoFunctionService implements CryptoFunctionService { const nodePassword = this.toNodeValue(password); const nodeSalt = this.toNodeBuffer(this.toUint8Buffer(salt)); + const argon2 = await import("argon2"); const hash = await argon2.hash(nodePassword, { salt: nodeSalt, raw: true, From b25dc6300cd8bd835cd6dd2319e1505d2734164f Mon Sep 17 00:00:00 2001 From: Jonas Hendrickx Date: Wed, 4 Dec 2024 15:40:57 +0100 Subject: [PATCH 20/46] Revert "[PM-13999] Show estimated tax for taxable countries (#12145)" (#12244) This reverts commit 1dce7f5ba0f66207faa2277f35e1fe3f64c74864. --- .../trial-billing-step.component.html | 2 +- .../premium/premium-v2.component.html | 3 +- .../premium/premium-v2.component.ts | 50 +- .../individual/premium/premium.component.html | 2 +- .../individual/premium/premium.component.ts | 51 +- .../change-plan-dialog.component.html | 41 +- .../change-plan-dialog.component.ts | 121 +--- .../organization-plans.component.html | 16 +- .../organization-plans.component.ts | 159 ++--- ...organization-payment-method.component.html | 15 + .../organization-payment-method.component.ts | 27 + .../adjust-payment-dialog-v2.component.html | 6 +- .../adjust-payment-dialog-v2.component.ts | 69 +- .../adjust-payment-dialog.component.html | 6 +- .../adjust-payment-dialog.component.ts | 59 +- .../shared/payment-method.component.html | 18 + .../shared/payment-method.component.ts | 21 +- .../billing/shared/tax-info.component.html | 60 +- .../app/billing/shared/tax-info.component.ts | 624 +++++++++++++++--- apps/web/src/locales/en/messages.json | 12 - .../providers/setup/setup.component.html | 2 +- .../providers/setup/setup.component.ts | 17 +- .../manage-tax-information.component.html | 88 +-- .../manage-tax-information.component.ts | 446 ++++++++++--- .../src/services/jslib-services.module.ts | 7 - .../abstractions/tax.service.abstraction.ts | 18 - .../models/domain/country-list-item.ts | 5 - .../common/src/billing/models/domain/index.ts | 1 - .../expanded-tax-info-update.request.ts | 4 - .../preview-individual-invoice.request.ts | 14 - .../preview-organization-invoice.request.ts | 25 - .../response/preview-invoice.response.ts | 16 - .../models/response/tax-id-types.response.ts | 28 - .../src/billing/services/tax.service.ts | 303 --------- 34 files changed, 1243 insertions(+), 1093 deletions(-) delete mode 100644 libs/common/src/billing/abstractions/tax.service.abstraction.ts delete mode 100644 libs/common/src/billing/models/domain/country-list-item.ts delete mode 100644 libs/common/src/billing/models/request/preview-individual-invoice.request.ts delete mode 100644 libs/common/src/billing/models/request/preview-organization-invoice.request.ts delete mode 100644 libs/common/src/billing/models/response/preview-invoice.response.ts delete mode 100644 libs/common/src/billing/models/response/tax-id-types.response.ts delete mode 100644 libs/common/src/billing/services/tax.service.ts diff --git a/apps/web/src/app/billing/accounts/trial-initiation/trial-billing-step.component.html b/apps/web/src/app/billing/accounts/trial-initiation/trial-billing-step.component.html index 2f983944b70..f927a7ca613 100644 --- a/apps/web/src/app/billing/accounts/trial-initiation/trial-billing-step.component.html +++ b/apps/web/src/app/billing/accounts/trial-initiation/trial-billing-step.component.html @@ -58,7 +58,7 @@ *ngIf="deprecateStripeSourcesAPI" [showAccountCredit]="false" > - +
+ diff --git a/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.ts b/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.ts index a6f04ea6862..e2178e7c02c 100644 --- a/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.ts +++ b/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.ts @@ -9,6 +9,7 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions"; import { PaymentMethodType } from "@bitwarden/common/billing/enums"; +import { ExpandedTaxInfoUpdateRequest } from "@bitwarden/common/billing/models/request/expanded-tax-info-update.request"; import { VerifyBankAccountRequest } from "@bitwarden/common/billing/models/request/verify-bank-account.request"; import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.response"; import { PaymentSourceResponse } from "@bitwarden/common/billing/models/response/payment-source.response"; @@ -180,6 +181,32 @@ export class OrganizationPaymentMethodComponent implements OnDestroy { } }; + protected updateTaxInformation = async (): Promise => { + this.taxInfoComponent.taxFormGroup.updateValueAndValidity(); + this.taxInfoComponent.taxFormGroup.markAllAsTouched(); + + if (this.taxInfoComponent.taxFormGroup.invalid) { + return; + } + + const request = new ExpandedTaxInfoUpdateRequest(); + request.country = this.taxInfoComponent.country; + request.postalCode = this.taxInfoComponent.postalCode; + request.taxId = this.taxInfoComponent.taxId; + request.line1 = this.taxInfoComponent.line1; + request.line2 = this.taxInfoComponent.line2; + request.city = this.taxInfoComponent.city; + request.state = this.taxInfoComponent.state; + + await this.billingApiService.updateOrganizationTaxInformation(this.organizationId, request); + + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("taxInfoUpdated"), + }); + }; + protected verifyBankAccount = async (request: VerifyBankAccountRequest): Promise => { await this.billingApiService.verifyOrganizationBankAccount(this.organizationId, request); this.toastService.showToast({ diff --git a/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog-v2.component.html b/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog-v2.component.html index 1767fb485d0..e41d3d961cd 100644 --- a/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog-v2.component.html +++ b/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog-v2.component.html @@ -5,11 +5,7 @@ [showBankAccount]="!!organizationId" [initialPaymentMethod]="initialPaymentMethod" > - + + + diff --git a/apps/web/src/app/billing/shared/payment-method.component.ts b/apps/web/src/app/billing/shared/payment-method.component.ts index 3159cfa2902..98e6efcd8bd 100644 --- a/apps/web/src/app/billing/shared/payment-method.component.ts +++ b/apps/web/src/app/billing/shared/payment-method.component.ts @@ -1,5 +1,5 @@ import { Location } from "@angular/common"; -import { Component, OnDestroy, OnInit } from "@angular/core"; +import { Component, OnDestroy, OnInit, ViewChild } from "@angular/core"; import { FormBuilder, FormControl, Validators } from "@angular/forms"; import { ActivatedRoute, Router } from "@angular/router"; import { lastValueFrom } from "rxjs"; @@ -14,6 +14,7 @@ import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/mode import { SubscriptionResponse } from "@bitwarden/common/billing/models/response/subscription.response"; import { VerifyBankRequest } from "@bitwarden/common/models/request/verify-bank.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 { SyncService } from "@bitwarden/common/platform/sync"; import { DialogService, ToastService } from "@bitwarden/components"; @@ -26,12 +27,15 @@ import { AdjustPaymentDialogResult, openAdjustPaymentDialog, } from "./adjust-payment-dialog/adjust-payment-dialog.component"; +import { TaxInfoComponent } from "./tax-info.component"; @Component({ templateUrl: "payment-method.component.html", }) // eslint-disable-next-line rxjs-angular/prefer-takeuntil export class PaymentMethodComponent implements OnInit, OnDestroy { + @ViewChild(TaxInfoComponent) taxInfo: TaxInfoComponent; + loading = false; firstLoaded = false; billing: BillingPaymentResponse; @@ -55,6 +59,7 @@ export class PaymentMethodComponent implements OnInit, OnDestroy { ]), }); + taxForm = this.formBuilder.group({}); launchPaymentModalAutomatically = false; protected freeTrialData: FreeTrial; @@ -65,6 +70,7 @@ export class PaymentMethodComponent implements OnInit, OnDestroy { protected platformUtilsService: PlatformUtilsService, private router: Router, private location: Location, + private logService: LogService, private route: ActivatedRoute, private formBuilder: FormBuilder, private dialogService: DialogService, @@ -190,6 +196,15 @@ export class PaymentMethodComponent implements OnInit, OnDestroy { await this.load(); }; + submitTaxInfo = async () => { + await this.taxInfo.submitTaxInfo(); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("taxInfoUpdated"), + }); + }; + determineOrgsWithUpcomingPaymentIssues() { this.freeTrialData = this.trialFlowService.checkForOrgsWithUpcomingPaymentIssues( this.organization, @@ -214,6 +229,10 @@ export class PaymentMethodComponent implements OnInit, OnDestroy { return this.organizationId != null; } + get headerClass() { + return this.forOrganization ? ["page-header"] : ["tabbed-header"]; + } + get paymentSourceClasses() { if (this.paymentSource == null) { return []; diff --git a/apps/web/src/app/billing/shared/tax-info.component.html b/apps/web/src/app/billing/shared/tax-info.component.html index 3955c0db816..82d5104a53a 100644 --- a/apps/web/src/app/billing/shared/tax-info.component.html +++ b/apps/web/src/app/billing/shared/tax-info.component.html @@ -13,41 +13,51 @@
-
+
{{ "zipPostalCode" | i18n }}
-
- - {{ "address1" | i18n }} - - +
+ + + {{ "includeVAT" | i18n }} +
-
- - {{ "address2" | i18n }} - - -
-
- - {{ "cityTown" | i18n }} - - -
-
- - {{ "stateProvince" | i18n }} - - -
-
+
+
+
{{ "taxIdNumber" | i18n }}
+
+
+ + {{ "address1" | i18n }} + + +
+
+ + {{ "address2" | i18n }} + + +
+
+ + {{ "cityTown" | i18n }} + + +
+
+ + {{ "stateProvince" | i18n }} + + +
+
diff --git a/apps/web/src/app/billing/shared/tax-info.component.ts b/apps/web/src/app/billing/shared/tax-info.component.ts index 48452528053..2cd8f7dc366 100644 --- a/apps/web/src/app/billing/shared/tax-info.component.ts +++ b/apps/web/src/app/billing/shared/tax-info.component.ts @@ -1,18 +1,29 @@ -import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core"; +import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core"; import { FormControl, FormGroup, Validators } from "@angular/forms"; import { ActivatedRoute } from "@angular/router"; import { Subject, takeUntil } from "rxjs"; -import { debounceTime } from "rxjs/operators"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; -import { TaxServiceAbstraction } from "@bitwarden/common/billing/abstractions/tax.service.abstraction"; -import { CountryListItem } from "@bitwarden/common/billing/models/domain"; import { ExpandedTaxInfoUpdateRequest } from "@bitwarden/common/billing/models/request/expanded-tax-info-update.request"; +import { TaxInfoUpdateRequest } from "@bitwarden/common/billing/models/request/tax-info-update.request"; +import { TaxInfoResponse } from "@bitwarden/common/billing/models/response/tax-info.response"; +import { TaxRateResponse } from "@bitwarden/common/billing/models/response/tax-rate.response"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { SharedModule } from "../../shared"; +type TaxInfoView = Omit & { + includeTaxId: boolean; + [key: string]: unknown; +}; + +type CountryList = { + name: string; + value: string; + disabled: boolean; +}; + @Component({ selector: "app-tax-info", templateUrl: "tax-info.component.html", @@ -20,64 +31,359 @@ import { SharedModule } from "../../shared"; imports: [SharedModule], }) // eslint-disable-next-line rxjs-angular/prefer-takeuntil -export class TaxInfoComponent implements OnInit, OnDestroy { +export class TaxInfoComponent implements OnInit { + @Input() trialFlow = false; + @Output() onCountryChanged = new EventEmitter(); private destroy$ = new Subject(); - @Input() trialFlow = false; - @Output() countryChanged = new EventEmitter(); - @Output() taxInformationChanged: EventEmitter = new EventEmitter(); - taxFormGroup = new FormGroup({ - country: new FormControl(null, [Validators.required]), - postalCode: new FormControl(null, [Validators.required]), - taxId: new FormControl(null), - line1: new FormControl(null), - line2: new FormControl(null), - city: new FormControl(null), - state: new FormControl(null), + country: new FormControl(null, [Validators.required]), + postalCode: new FormControl(null), + includeTaxId: new FormControl(null), + taxId: new FormControl(null), + line1: new FormControl(null), + line2: new FormControl(null), + city: new FormControl(null), + state: new FormControl(null), }); - protected isTaxSupported: boolean; - loading = true; organizationId: string; providerId: string; - countryList: CountryListItem[] = this.taxService.getCountries(); + taxInfo: TaxInfoView = { + taxId: null, + line1: null, + line2: null, + city: null, + state: null, + postalCode: null, + country: "US", + includeTaxId: false, + }; + countryList: CountryList[] = [ + { name: "-- Select --", value: "", disabled: false }, + { name: "United States", value: "US", disabled: false }, + { name: "China", value: "CN", disabled: false }, + { name: "France", value: "FR", disabled: false }, + { name: "Germany", value: "DE", disabled: false }, + { name: "Canada", value: "CA", disabled: false }, + { name: "United Kingdom", value: "GB", disabled: false }, + { name: "Australia", value: "AU", disabled: false }, + { name: "India", value: "IN", disabled: false }, + { name: "", value: "-", disabled: true }, + { name: "Afghanistan", value: "AF", disabled: false }, + { name: "Åland Islands", value: "AX", disabled: false }, + { name: "Albania", value: "AL", disabled: false }, + { name: "Algeria", value: "DZ", disabled: false }, + { name: "American Samoa", value: "AS", disabled: false }, + { name: "Andorra", value: "AD", disabled: false }, + { name: "Angola", value: "AO", disabled: false }, + { name: "Anguilla", value: "AI", disabled: false }, + { name: "Antarctica", value: "AQ", disabled: false }, + { name: "Antigua and Barbuda", value: "AG", disabled: false }, + { name: "Argentina", value: "AR", disabled: false }, + { name: "Armenia", value: "AM", disabled: false }, + { name: "Aruba", value: "AW", disabled: false }, + { name: "Austria", value: "AT", disabled: false }, + { name: "Azerbaijan", value: "AZ", disabled: false }, + { name: "Bahamas", value: "BS", disabled: false }, + { name: "Bahrain", value: "BH", disabled: false }, + { name: "Bangladesh", value: "BD", disabled: false }, + { name: "Barbados", value: "BB", disabled: false }, + { name: "Belarus", value: "BY", disabled: false }, + { name: "Belgium", value: "BE", disabled: false }, + { name: "Belize", value: "BZ", disabled: false }, + { name: "Benin", value: "BJ", disabled: false }, + { name: "Bermuda", value: "BM", disabled: false }, + { name: "Bhutan", value: "BT", disabled: false }, + { name: "Bolivia, Plurinational State of", value: "BO", disabled: false }, + { name: "Bonaire, Sint Eustatius and Saba", value: "BQ", disabled: false }, + { name: "Bosnia and Herzegovina", value: "BA", disabled: false }, + { name: "Botswana", value: "BW", disabled: false }, + { name: "Bouvet Island", value: "BV", disabled: false }, + { name: "Brazil", value: "BR", disabled: false }, + { name: "British Indian Ocean Territory", value: "IO", disabled: false }, + { name: "Brunei Darussalam", value: "BN", disabled: false }, + { name: "Bulgaria", value: "BG", disabled: false }, + { name: "Burkina Faso", value: "BF", disabled: false }, + { name: "Burundi", value: "BI", disabled: false }, + { name: "Cambodia", value: "KH", disabled: false }, + { name: "Cameroon", value: "CM", disabled: false }, + { name: "Cape Verde", value: "CV", disabled: false }, + { name: "Cayman Islands", value: "KY", disabled: false }, + { name: "Central African Republic", value: "CF", disabled: false }, + { name: "Chad", value: "TD", disabled: false }, + { name: "Chile", value: "CL", disabled: false }, + { name: "Christmas Island", value: "CX", disabled: false }, + { name: "Cocos (Keeling) Islands", value: "CC", disabled: false }, + { name: "Colombia", value: "CO", disabled: false }, + { name: "Comoros", value: "KM", disabled: false }, + { name: "Congo", value: "CG", disabled: false }, + { name: "Congo, the Democratic Republic of the", value: "CD", disabled: false }, + { name: "Cook Islands", value: "CK", disabled: false }, + { name: "Costa Rica", value: "CR", disabled: false }, + { name: "Côte d'Ivoire", value: "CI", disabled: false }, + { name: "Croatia", value: "HR", disabled: false }, + { name: "Cuba", value: "CU", disabled: false }, + { name: "Curaçao", value: "CW", disabled: false }, + { name: "Cyprus", value: "CY", disabled: false }, + { name: "Czech Republic", value: "CZ", disabled: false }, + { name: "Denmark", value: "DK", disabled: false }, + { name: "Djibouti", value: "DJ", disabled: false }, + { name: "Dominica", value: "DM", disabled: false }, + { name: "Dominican Republic", value: "DO", disabled: false }, + { name: "Ecuador", value: "EC", disabled: false }, + { name: "Egypt", value: "EG", disabled: false }, + { name: "El Salvador", value: "SV", disabled: false }, + { name: "Equatorial Guinea", value: "GQ", disabled: false }, + { name: "Eritrea", value: "ER", disabled: false }, + { name: "Estonia", value: "EE", disabled: false }, + { name: "Ethiopia", value: "ET", disabled: false }, + { name: "Falkland Islands (Malvinas)", value: "FK", disabled: false }, + { name: "Faroe Islands", value: "FO", disabled: false }, + { name: "Fiji", value: "FJ", disabled: false }, + { name: "Finland", value: "FI", disabled: false }, + { name: "French Guiana", value: "GF", disabled: false }, + { name: "French Polynesia", value: "PF", disabled: false }, + { name: "French Southern Territories", value: "TF", disabled: false }, + { name: "Gabon", value: "GA", disabled: false }, + { name: "Gambia", value: "GM", disabled: false }, + { name: "Georgia", value: "GE", disabled: false }, + { name: "Ghana", value: "GH", disabled: false }, + { name: "Gibraltar", value: "GI", disabled: false }, + { name: "Greece", value: "GR", disabled: false }, + { name: "Greenland", value: "GL", disabled: false }, + { name: "Grenada", value: "GD", disabled: false }, + { name: "Guadeloupe", value: "GP", disabled: false }, + { name: "Guam", value: "GU", disabled: false }, + { name: "Guatemala", value: "GT", disabled: false }, + { name: "Guernsey", value: "GG", disabled: false }, + { name: "Guinea", value: "GN", disabled: false }, + { name: "Guinea-Bissau", value: "GW", disabled: false }, + { name: "Guyana", value: "GY", disabled: false }, + { name: "Haiti", value: "HT", disabled: false }, + { name: "Heard Island and McDonald Islands", value: "HM", disabled: false }, + { name: "Holy See (Vatican City State)", value: "VA", disabled: false }, + { name: "Honduras", value: "HN", disabled: false }, + { name: "Hong Kong", value: "HK", disabled: false }, + { name: "Hungary", value: "HU", disabled: false }, + { name: "Iceland", value: "IS", disabled: false }, + { name: "Indonesia", value: "ID", disabled: false }, + { name: "Iran, Islamic Republic of", value: "IR", disabled: false }, + { name: "Iraq", value: "IQ", disabled: false }, + { name: "Ireland", value: "IE", disabled: false }, + { name: "Isle of Man", value: "IM", disabled: false }, + { name: "Israel", value: "IL", disabled: false }, + { name: "Italy", value: "IT", disabled: false }, + { name: "Jamaica", value: "JM", disabled: false }, + { name: "Japan", value: "JP", disabled: false }, + { name: "Jersey", value: "JE", disabled: false }, + { name: "Jordan", value: "JO", disabled: false }, + { name: "Kazakhstan", value: "KZ", disabled: false }, + { name: "Kenya", value: "KE", disabled: false }, + { name: "Kiribati", value: "KI", disabled: false }, + { name: "Korea, Democratic People's Republic of", value: "KP", disabled: false }, + { name: "Korea, Republic of", value: "KR", disabled: false }, + { name: "Kuwait", value: "KW", disabled: false }, + { name: "Kyrgyzstan", value: "KG", disabled: false }, + { name: "Lao People's Democratic Republic", value: "LA", disabled: false }, + { name: "Latvia", value: "LV", disabled: false }, + { name: "Lebanon", value: "LB", disabled: false }, + { name: "Lesotho", value: "LS", disabled: false }, + { name: "Liberia", value: "LR", disabled: false }, + { name: "Libya", value: "LY", disabled: false }, + { name: "Liechtenstein", value: "LI", disabled: false }, + { name: "Lithuania", value: "LT", disabled: false }, + { name: "Luxembourg", value: "LU", disabled: false }, + { name: "Macao", value: "MO", disabled: false }, + { name: "Macedonia, the former Yugoslav Republic of", value: "MK", disabled: false }, + { name: "Madagascar", value: "MG", disabled: false }, + { name: "Malawi", value: "MW", disabled: false }, + { name: "Malaysia", value: "MY", disabled: false }, + { name: "Maldives", value: "MV", disabled: false }, + { name: "Mali", value: "ML", disabled: false }, + { name: "Malta", value: "MT", disabled: false }, + { name: "Marshall Islands", value: "MH", disabled: false }, + { name: "Martinique", value: "MQ", disabled: false }, + { name: "Mauritania", value: "MR", disabled: false }, + { name: "Mauritius", value: "MU", disabled: false }, + { name: "Mayotte", value: "YT", disabled: false }, + { name: "Mexico", value: "MX", disabled: false }, + { name: "Micronesia, Federated States of", value: "FM", disabled: false }, + { name: "Moldova, Republic of", value: "MD", disabled: false }, + { name: "Monaco", value: "MC", disabled: false }, + { name: "Mongolia", value: "MN", disabled: false }, + { name: "Montenegro", value: "ME", disabled: false }, + { name: "Montserrat", value: "MS", disabled: false }, + { name: "Morocco", value: "MA", disabled: false }, + { name: "Mozambique", value: "MZ", disabled: false }, + { name: "Myanmar", value: "MM", disabled: false }, + { name: "Namibia", value: "NA", disabled: false }, + { name: "Nauru", value: "NR", disabled: false }, + { name: "Nepal", value: "NP", disabled: false }, + { name: "Netherlands", value: "NL", disabled: false }, + { name: "New Caledonia", value: "NC", disabled: false }, + { name: "New Zealand", value: "NZ", disabled: false }, + { name: "Nicaragua", value: "NI", disabled: false }, + { name: "Niger", value: "NE", disabled: false }, + { name: "Nigeria", value: "NG", disabled: false }, + { name: "Niue", value: "NU", disabled: false }, + { name: "Norfolk Island", value: "NF", disabled: false }, + { name: "Northern Mariana Islands", value: "MP", disabled: false }, + { name: "Norway", value: "NO", disabled: false }, + { name: "Oman", value: "OM", disabled: false }, + { name: "Pakistan", value: "PK", disabled: false }, + { name: "Palau", value: "PW", disabled: false }, + { name: "Palestinian Territory, Occupied", value: "PS", disabled: false }, + { name: "Panama", value: "PA", disabled: false }, + { name: "Papua New Guinea", value: "PG", disabled: false }, + { name: "Paraguay", value: "PY", disabled: false }, + { name: "Peru", value: "PE", disabled: false }, + { name: "Philippines", value: "PH", disabled: false }, + { name: "Pitcairn", value: "PN", disabled: false }, + { name: "Poland", value: "PL", disabled: false }, + { name: "Portugal", value: "PT", disabled: false }, + { name: "Puerto Rico", value: "PR", disabled: false }, + { name: "Qatar", value: "QA", disabled: false }, + { name: "Réunion", value: "RE", disabled: false }, + { name: "Romania", value: "RO", disabled: false }, + { name: "Russian Federation", value: "RU", disabled: false }, + { name: "Rwanda", value: "RW", disabled: false }, + { name: "Saint Barthélemy", value: "BL", disabled: false }, + { name: "Saint Helena, Ascension and Tristan da Cunha", value: "SH", disabled: false }, + { name: "Saint Kitts and Nevis", value: "KN", disabled: false }, + { name: "Saint Lucia", value: "LC", disabled: false }, + { name: "Saint Martin (French part)", value: "MF", disabled: false }, + { name: "Saint Pierre and Miquelon", value: "PM", disabled: false }, + { name: "Saint Vincent and the Grenadines", value: "VC", disabled: false }, + { name: "Samoa", value: "WS", disabled: false }, + { name: "San Marino", value: "SM", disabled: false }, + { name: "Sao Tome and Principe", value: "ST", disabled: false }, + { name: "Saudi Arabia", value: "SA", disabled: false }, + { name: "Senegal", value: "SN", disabled: false }, + { name: "Serbia", value: "RS", disabled: false }, + { name: "Seychelles", value: "SC", disabled: false }, + { name: "Sierra Leone", value: "SL", disabled: false }, + { name: "Singapore", value: "SG", disabled: false }, + { name: "Sint Maarten (Dutch part)", value: "SX", disabled: false }, + { name: "Slovakia", value: "SK", disabled: false }, + { name: "Slovenia", value: "SI", disabled: false }, + { name: "Solomon Islands", value: "SB", disabled: false }, + { name: "Somalia", value: "SO", disabled: false }, + { name: "South Africa", value: "ZA", disabled: false }, + { name: "South Georgia and the South Sandwich Islands", value: "GS", disabled: false }, + { name: "South Sudan", value: "SS", disabled: false }, + { name: "Spain", value: "ES", disabled: false }, + { name: "Sri Lanka", value: "LK", disabled: false }, + { name: "Sudan", value: "SD", disabled: false }, + { name: "Suriname", value: "SR", disabled: false }, + { name: "Svalbard and Jan Mayen", value: "SJ", disabled: false }, + { name: "Swaziland", value: "SZ", disabled: false }, + { name: "Sweden", value: "SE", disabled: false }, + { name: "Switzerland", value: "CH", disabled: false }, + { name: "Syrian Arab Republic", value: "SY", disabled: false }, + { name: "Taiwan", value: "TW", disabled: false }, + { name: "Tajikistan", value: "TJ", disabled: false }, + { name: "Tanzania, United Republic of", value: "TZ", disabled: false }, + { name: "Thailand", value: "TH", disabled: false }, + { name: "Timor-Leste", value: "TL", disabled: false }, + { name: "Togo", value: "TG", disabled: false }, + { name: "Tokelau", value: "TK", disabled: false }, + { name: "Tonga", value: "TO", disabled: false }, + { name: "Trinidad and Tobago", value: "TT", disabled: false }, + { name: "Tunisia", value: "TN", disabled: false }, + { name: "Turkey", value: "TR", disabled: false }, + { name: "Turkmenistan", value: "TM", disabled: false }, + { name: "Turks and Caicos Islands", value: "TC", disabled: false }, + { name: "Tuvalu", value: "TV", disabled: false }, + { name: "Uganda", value: "UG", disabled: false }, + { name: "Ukraine", value: "UA", disabled: false }, + { name: "United Arab Emirates", value: "AE", disabled: false }, + { name: "United States Minor Outlying Islands", value: "UM", disabled: false }, + { name: "Uruguay", value: "UY", disabled: false }, + { name: "Uzbekistan", value: "UZ", disabled: false }, + { name: "Vanuatu", value: "VU", disabled: false }, + { name: "Venezuela, Bolivarian Republic of", value: "VE", disabled: false }, + { name: "Viet Nam", value: "VN", disabled: false }, + { name: "Virgin Islands, British", value: "VG", disabled: false }, + { name: "Virgin Islands, U.S.", value: "VI", disabled: false }, + { name: "Wallis and Futuna", value: "WF", disabled: false }, + { name: "Western Sahara", value: "EH", disabled: false }, + { name: "Yemen", value: "YE", disabled: false }, + { name: "Zambia", value: "ZM", disabled: false }, + { name: "Zimbabwe", value: "ZW", disabled: false }, + ]; + taxRates: TaxRateResponse[]; constructor( private apiService: ApiService, private route: ActivatedRoute, private logService: LogService, private organizationApiService: OrganizationApiServiceAbstraction, - private taxService: TaxServiceAbstraction, ) {} get country(): string { - return this.taxFormGroup.controls.country.value; + return this.taxFormGroup.get("country").value; + } + + set country(country: string) { + this.taxFormGroup.get("country").setValue(country); } get postalCode(): string { - return this.taxFormGroup.controls.postalCode.value; + return this.taxFormGroup.get("postalCode").value; + } + + set postalCode(postalCode: string) { + this.taxFormGroup.get("postalCode").setValue(postalCode); + } + + get includeTaxId(): boolean { + return this.taxFormGroup.get("includeTaxId").value; + } + + set includeTaxId(includeTaxId: boolean) { + this.taxFormGroup.get("includeTaxId").setValue(includeTaxId); } get taxId(): string { - return this.taxFormGroup.controls.taxId.value; + return this.taxFormGroup.get("taxId").value; + } + + set taxId(taxId: string) { + this.taxFormGroup.get("taxId").setValue(taxId); } get line1(): string { - return this.taxFormGroup.controls.line1.value; + return this.taxFormGroup.get("line1").value; + } + + set line1(line1: string) { + this.taxFormGroup.get("line1").setValue(line1); } get line2(): string { - return this.taxFormGroup.controls.line2.value; + return this.taxFormGroup.get("line2").value; + } + + set line2(line2: string) { + this.taxFormGroup.get("line2").setValue(line2); } get city(): string { - return this.taxFormGroup.controls.city.value; + return this.taxFormGroup.get("city").value; + } + + set city(city: string) { + this.taxFormGroup.get("city").setValue(city); } get state(): string { - return this.taxFormGroup.controls.state.value; + return this.taxFormGroup.get("state").value; + } + + set state(state: string) { + this.taxFormGroup.get("state").setValue(state); } async ngOnInit() { @@ -94,13 +400,22 @@ export class TaxInfoComponent implements OnInit, OnDestroy { try { const taxInfo = await this.organizationApiService.getTaxInfo(this.organizationId); if (taxInfo) { - this.taxFormGroup.controls.taxId.setValue(taxInfo.taxId); - this.taxFormGroup.controls.state.setValue(taxInfo.state); - this.taxFormGroup.controls.line1.setValue(taxInfo.line1); - this.taxFormGroup.controls.line2.setValue(taxInfo.line2); - this.taxFormGroup.controls.city.setValue(taxInfo.city); - this.taxFormGroup.controls.postalCode.setValue(taxInfo.postalCode); - this.taxFormGroup.controls.country.setValue(taxInfo.country); + this.taxId = taxInfo.taxId; + this.state = taxInfo.state; + this.line1 = taxInfo.line1; + this.line2 = taxInfo.line2; + this.city = taxInfo.city; + this.state = taxInfo.state; + this.postalCode = taxInfo.postalCode; + this.country = taxInfo.country || "US"; + this.includeTaxId = + this.countrySupportsTax(this.country) && + (!!taxInfo.taxId || + !!taxInfo.line1 || + !!taxInfo.line2 || + !!taxInfo.city || + !!taxInfo.state); + this.setTaxInfoObject(); } } catch (e) { this.logService.error(e); @@ -109,79 +424,119 @@ export class TaxInfoComponent implements OnInit, OnDestroy { try { const taxInfo = await this.apiService.getTaxInfo(); if (taxInfo) { - this.taxFormGroup.controls.postalCode.setValue(taxInfo.postalCode); - this.taxFormGroup.controls.country.setValue(taxInfo.country); + this.postalCode = taxInfo.postalCode; + this.country = taxInfo.country || "US"; } + this.setTaxInfoObject(); } catch (e) { this.logService.error(e); } } - this.isTaxSupported = await this.taxService.isCountrySupported( - this.taxFormGroup.controls.country.value, - ); + if (this.country === "US") { + this.taxFormGroup.get("postalCode").setValidators([Validators.required]); + this.taxFormGroup.get("postalCode").updateValueAndValidity(); + } - this.countryChanged.emit(); + if (this.country !== "US") { + this.onCountryChanged.emit(); + } }); - this.taxFormGroup.controls.country.valueChanges - .pipe(debounceTime(1000), takeUntil(this.destroy$)) + this.taxFormGroup + .get("country") + .valueChanges.pipe(takeUntil(this.destroy$)) .subscribe((value) => { - this.taxService - .isCountrySupported(this.taxFormGroup.controls.country.value) - .then((isSupported) => { - this.isTaxSupported = isSupported; - }) - .catch(() => { - this.isTaxSupported = false; - }) - .finally(() => { - if (!this.isTaxSupported) { - this.taxFormGroup.controls.taxId.setValue(null); - this.taxFormGroup.controls.line1.setValue(null); - this.taxFormGroup.controls.line2.setValue(null); - this.taxFormGroup.controls.city.setValue(null); - this.taxFormGroup.controls.state.setValue(null); - } - - this.countryChanged.emit(); - }); - this.taxInformationChanged.emit(); + if (value === "US") { + this.taxFormGroup.get("postalCode").setValidators([Validators.required]); + } else { + this.taxFormGroup.get("postalCode").clearValidators(); + } + this.taxFormGroup.get("postalCode").updateValueAndValidity(); + this.setTaxInfoObject(); + this.changeCountry(); }); - this.taxFormGroup.controls.postalCode.valueChanges - .pipe(debounceTime(1000), takeUntil(this.destroy$)) - .subscribe(() => { - this.taxInformationChanged.emit(); - }); - - this.taxFormGroup.controls.taxId.valueChanges - .pipe(debounceTime(1000), takeUntil(this.destroy$)) - .subscribe(() => { - this.taxInformationChanged.emit(); - }); - - this.loading = false; + try { + const taxRates = await this.apiService.getTaxRates(); + if (taxRates) { + this.taxRates = taxRates.data; + } + } catch (e) { + this.logService.error(e); + } finally { + this.loading = false; + } } - ngOnDestroy() { - this.destroy$.next(); - this.destroy$.complete(); + get taxRate() { + if (this.taxRates != null) { + const localTaxRate = this.taxRates.find( + (x) => x.country === this.country && x.postalCode === this.postalCode, + ); + return localTaxRate?.rate ?? null; + } + } + + setTaxInfoObject() { + this.taxInfo.country = this.country; + this.taxInfo.postalCode = this.postalCode; + this.taxInfo.includeTaxId = this.includeTaxId; + this.taxInfo.taxId = this.taxId; + this.taxInfo.line1 = this.line1; + this.taxInfo.line2 = this.line2; + this.taxInfo.city = this.city; + this.taxInfo.state = this.state; + } + + get showTaxIdCheckbox() { + return ( + (this.organizationId || this.providerId) && + this.country !== "US" && + this.countrySupportsTax(this.taxInfo.country) + ); + } + + get showTaxIdFields() { + return ( + (this.organizationId || this.providerId) && + this.includeTaxId && + this.countrySupportsTax(this.country) + ); + } + + getTaxInfoRequest(): TaxInfoUpdateRequest { + if (this.organizationId || this.providerId) { + const request = new ExpandedTaxInfoUpdateRequest(); + request.country = this.country; + request.postalCode = this.postalCode; + + if (this.includeTaxId) { + request.taxId = this.taxId; + request.line1 = this.line1; + request.line2 = this.line2; + request.city = this.city; + request.state = this.state; + } else { + request.taxId = null; + request.line1 = null; + request.line2 = null; + request.city = null; + request.state = null; + } + return request; + } else { + const request = new TaxInfoUpdateRequest(); + request.postalCode = this.postalCode; + request.country = this.country; + return request; + } } submitTaxInfo(): Promise { this.taxFormGroup.updateValueAndValidity(); this.taxFormGroup.markAllAsTouched(); - - const request = new ExpandedTaxInfoUpdateRequest(); - request.country = this.country; - request.postalCode = this.postalCode; - request.taxId = this.taxId; - request.line1 = this.line1; - request.line2 = this.line2; - request.city = this.city; - request.state = this.state; - + const request = this.getTaxInfoRequest(); return this.organizationId ? this.organizationApiService.updateTaxInfo( this.organizationId, @@ -189,4 +544,97 @@ export class TaxInfoComponent implements OnInit, OnDestroy { ) : this.apiService.putTaxInfo(request); } + + changeCountry() { + if (!this.countrySupportsTax(this.country)) { + this.includeTaxId = false; + this.taxId = null; + this.line1 = null; + this.line2 = null; + this.city = null; + this.state = null; + this.setTaxInfoObject(); + } + this.onCountryChanged.emit(); + } + + countrySupportsTax(countryCode: string) { + return this.taxSupportedCountryCodes.includes(countryCode); + } + + private taxSupportedCountryCodes: string[] = [ + "CN", + "FR", + "DE", + "CA", + "GB", + "AU", + "IN", + "AD", + "AR", + "AT", + "BE", + "BO", + "BR", + "BG", + "CL", + "CO", + "CR", + "HR", + "CY", + "CZ", + "DK", + "DO", + "EC", + "EG", + "SV", + "EE", + "FI", + "GE", + "GR", + "HK", + "HU", + "IS", + "ID", + "IQ", + "IE", + "IL", + "IT", + "JP", + "KE", + "KR", + "LV", + "LI", + "LT", + "LU", + "MY", + "MT", + "MX", + "NL", + "NZ", + "NO", + "PE", + "PH", + "PL", + "PT", + "RO", + "RU", + "SA", + "RS", + "SG", + "SK", + "SI", + "ZA", + "ES", + "SE", + "CH", + "TW", + "TH", + "TR", + "UA", + "AE", + "UY", + "VE", + "VN", + ]; } diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 25172f7c779..cab0e703a7d 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -9224,18 +9224,6 @@ "updatedTaxInformation": { "message": "Updated tax information" }, - "billingInvalidTaxIdError": { - "message": "Invalid tax ID, if you believe this is an error please contact support." - }, - "billingTaxIdTypeInferenceError": { - "message": "We were unable to validate your tax ID, if you believe this is an error please contact support." - }, - "billingPreviewInvalidTaxIdError": { - "message": "Invalid tax ID, if you believe this is an error please contact support." - }, - "billingPreviewInvoiceError": { - "message": "An error occurred while previewing the invoice. Please try again later." - }, "unverified": { "message": "Unverified" }, 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 74aa468c42e..33a20444c2b 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.html +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.html @@ -29,7 +29,7 @@
- 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 079174b48b5..72d954e8cdc 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 @@ -109,7 +109,9 @@ export class SetupComponent implements OnInit, OnDestroy { try { this.formGroup.markAllAsTouched(); - if (!this.manageTaxInformationComponent.validate() || !this.formGroup.valid) { + const formIsValid = this.formGroup.valid && this.manageTaxInformationComponent.touch(); + + if (!formIsValid) { return; } @@ -127,11 +129,14 @@ export class SetupComponent implements OnInit, OnDestroy { request.taxInfo.country = taxInformation.country; request.taxInfo.postalCode = taxInformation.postalCode; - request.taxInfo.taxId = taxInformation.taxId; - request.taxInfo.line1 = taxInformation.line1; - request.taxInfo.line2 = taxInformation.line2; - request.taxInfo.city = taxInformation.city; - request.taxInfo.state = taxInformation.state; + + if (taxInformation.includeTaxId) { + request.taxInfo.taxId = taxInformation.taxId; + request.taxInfo.line1 = taxInformation.line1; + request.taxInfo.line2 = taxInformation.line2; + request.taxInfo.city = taxInformation.city; + request.taxInfo.state = taxInformation.state; + } const provider = await this.providerApiService.postProviderSetup(this.providerId, request); diff --git a/libs/angular/src/billing/components/manage-tax-information/manage-tax-information.component.html b/libs/angular/src/billing/components/manage-tax-information/manage-tax-information.component.html index fbd8af4f8b9..0b041bd4c06 100644 --- a/libs/angular/src/billing/components/manage-tax-information/manage-tax-information.component.html +++ b/libs/angular/src/billing/components/manage-tax-information/manage-tax-information.component.html @@ -1,7 +1,7 @@
- + {{ "country" | i18n }}
- + {{ "zipPostalCode" | i18n }}
- -
- - {{ "address1" | i18n }} - - -
-
- - {{ "address2" | i18n }} - - -
-
- - {{ "cityTown" | i18n }} - - -
-
- - {{ "stateProvince" | i18n }} - - -
-
- - {{ "taxIdNumber" | i18n }} - - -
-
-
- +
+ + + {{ "includeVAT" | i18n }} +
+
+
+ + {{ "taxIdNumber" | i18n }} + + +
+
+
+
+ + {{ "address1" | i18n }} + + +
+
+ + {{ "address2" | i18n }} + + +
+
+ + {{ "cityTown" | i18n }} + + +
+
+ + {{ "stateProvince" | i18n }} + + +
+
+ diff --git a/libs/angular/src/billing/components/manage-tax-information/manage-tax-information.component.ts b/libs/angular/src/billing/components/manage-tax-information/manage-tax-information.component.ts index 2f79c6f5397..e73a6968607 100644 --- a/libs/angular/src/billing/components/manage-tax-information/manage-tax-information.component.ts +++ b/libs/angular/src/billing/components/manage-tax-information/manage-tax-information.component.ts @@ -1,10 +1,14 @@ import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core"; import { FormBuilder, Validators } from "@angular/forms"; import { Subject, takeUntil } from "rxjs"; -import { debounceTime } from "rxjs/operators"; -import { TaxServiceAbstraction } from "@bitwarden/common/billing/abstractions/tax.service.abstraction"; -import { CountryListItem, TaxInformation } from "@bitwarden/common/billing/models/domain"; +import { TaxInformation } from "@bitwarden/common/billing/models/domain"; + +type Country = { + name: string; + value: string; + disabled: boolean; +}; @Component({ selector: "app-manage-tax-information", @@ -13,22 +17,12 @@ import { CountryListItem, TaxInformation } from "@bitwarden/common/billing/model export class ManageTaxInformationComponent implements OnInit, OnDestroy { @Input() startWith: TaxInformation; @Input() onSubmit?: (taxInformation: TaxInformation) => Promise; - - /** - * Emits when the tax information has changed. - */ - @Output() taxInformationChanged = new EventEmitter(); - - /** - * Emits when the tax information has been updated. - */ @Output() taxInformationUpdated = new EventEmitter(); - private taxInformation: TaxInformation; - protected formGroup = this.formBuilder.group({ country: ["", Validators.required], postalCode: ["", Validators.required], + includeTaxId: false, taxId: "", line1: "", line2: "", @@ -36,20 +30,16 @@ export class ManageTaxInformationComponent implements OnInit, OnDestroy { state: "", }); - protected isTaxSupported: boolean; - private destroy$ = new Subject(); - protected readonly countries: CountryListItem[] = this.taxService.getCountries(); + private taxInformation: TaxInformation; - constructor( - private formBuilder: FormBuilder, - private taxService: TaxServiceAbstraction, - ) {} + constructor(private formBuilder: FormBuilder) {} - getTaxInformation(): TaxInformation { - return this.taxInformation; - } + getTaxInformation = (): TaxInformation & { includeTaxId: boolean } => ({ + ...this.taxInformation, + includeTaxId: this.formGroup.value.includeTaxId, + }); submit = async () => { this.formGroup.markAllAsTouched(); @@ -60,28 +50,23 @@ export class ManageTaxInformationComponent implements OnInit, OnDestroy { this.taxInformationUpdated.emit(); }; - validate = (): boolean => { + touch = (): boolean => { this.formGroup.markAllAsTouched(); return this.formGroup.valid; }; async ngOnInit() { if (this.startWith) { - this.formGroup.controls.country.setValue(this.startWith.country); - this.formGroup.controls.postalCode.setValue(this.startWith.postalCode); - - this.isTaxSupported = - this.startWith && this.startWith.country - ? await this.taxService.isCountrySupported(this.startWith.country) - : false; - - if (this.isTaxSupported) { - this.formGroup.controls.taxId.setValue(this.startWith.taxId); - this.formGroup.controls.line1.setValue(this.startWith.line1); - this.formGroup.controls.line2.setValue(this.startWith.line2); - this.formGroup.controls.city.setValue(this.startWith.city); - this.formGroup.controls.state.setValue(this.startWith.state); - } + this.formGroup.patchValue({ + ...this.startWith, + includeTaxId: + this.countrySupportsTax(this.startWith.country) && + (!!this.startWith.taxId || + !!this.startWith.line1 || + !!this.startWith.line2 || + !!this.startWith.city || + !!this.startWith.state), + }); } this.formGroup.valueChanges.pipe(takeUntil(this.destroy$)).subscribe((values) => { @@ -95,47 +80,354 @@ export class ManageTaxInformationComponent implements OnInit, OnDestroy { state: values.state, }; }); - - this.formGroup.controls.country.valueChanges - .pipe(debounceTime(1000), takeUntil(this.destroy$)) - .subscribe((country: string) => { - this.taxService - .isCountrySupported(country) - .then((isSupported) => (this.isTaxSupported = isSupported)) - .catch(() => (this.isTaxSupported = false)) - .finally(() => { - if (!this.isTaxSupported) { - this.formGroup.controls.taxId.setValue(null); - this.formGroup.controls.line1.setValue(null); - this.formGroup.controls.line2.setValue(null); - this.formGroup.controls.city.setValue(null); - this.formGroup.controls.state.setValue(null); - } - if (this.taxInformationChanged) { - this.taxInformationChanged.emit(this.taxInformation); - } - }); - }); - - this.formGroup.controls.postalCode.valueChanges - .pipe(debounceTime(1000), takeUntil(this.destroy$)) - .subscribe(() => { - if (this.taxInformationChanged) { - this.taxInformationChanged.emit(this.taxInformation); - } - }); - - this.formGroup.controls.taxId.valueChanges - .pipe(debounceTime(1000), takeUntil(this.destroy$)) - .subscribe(() => { - if (this.taxInformationChanged) { - this.taxInformationChanged.emit(this.taxInformation); - } - }); } ngOnDestroy() { this.destroy$.next(); this.destroy$.complete(); } + + protected countrySupportsTax(countryCode: string) { + return this.taxSupportedCountryCodes.includes(countryCode); + } + + protected get includeTaxIdIsSelected() { + return this.formGroup.value.includeTaxId; + } + + protected get selectionSupportsAdditionalOptions() { + return ( + this.formGroup.value.country !== "US" && this.countrySupportsTax(this.formGroup.value.country) + ); + } + + protected countries: Country[] = [ + { name: "-- Select --", value: "", disabled: false }, + { name: "United States", value: "US", disabled: false }, + { name: "China", value: "CN", disabled: false }, + { name: "France", value: "FR", disabled: false }, + { name: "Germany", value: "DE", disabled: false }, + { name: "Canada", value: "CA", disabled: false }, + { name: "United Kingdom", value: "GB", disabled: false }, + { name: "Australia", value: "AU", disabled: false }, + { name: "India", value: "IN", disabled: false }, + { name: "", value: "-", disabled: true }, + { name: "Afghanistan", value: "AF", disabled: false }, + { name: "Åland Islands", value: "AX", disabled: false }, + { name: "Albania", value: "AL", disabled: false }, + { name: "Algeria", value: "DZ", disabled: false }, + { name: "American Samoa", value: "AS", disabled: false }, + { name: "Andorra", value: "AD", disabled: false }, + { name: "Angola", value: "AO", disabled: false }, + { name: "Anguilla", value: "AI", disabled: false }, + { name: "Antarctica", value: "AQ", disabled: false }, + { name: "Antigua and Barbuda", value: "AG", disabled: false }, + { name: "Argentina", value: "AR", disabled: false }, + { name: "Armenia", value: "AM", disabled: false }, + { name: "Aruba", value: "AW", disabled: false }, + { name: "Austria", value: "AT", disabled: false }, + { name: "Azerbaijan", value: "AZ", disabled: false }, + { name: "Bahamas", value: "BS", disabled: false }, + { name: "Bahrain", value: "BH", disabled: false }, + { name: "Bangladesh", value: "BD", disabled: false }, + { name: "Barbados", value: "BB", disabled: false }, + { name: "Belarus", value: "BY", disabled: false }, + { name: "Belgium", value: "BE", disabled: false }, + { name: "Belize", value: "BZ", disabled: false }, + { name: "Benin", value: "BJ", disabled: false }, + { name: "Bermuda", value: "BM", disabled: false }, + { name: "Bhutan", value: "BT", disabled: false }, + { name: "Bolivia, Plurinational State of", value: "BO", disabled: false }, + { name: "Bonaire, Sint Eustatius and Saba", value: "BQ", disabled: false }, + { name: "Bosnia and Herzegovina", value: "BA", disabled: false }, + { name: "Botswana", value: "BW", disabled: false }, + { name: "Bouvet Island", value: "BV", disabled: false }, + { name: "Brazil", value: "BR", disabled: false }, + { name: "British Indian Ocean Territory", value: "IO", disabled: false }, + { name: "Brunei Darussalam", value: "BN", disabled: false }, + { name: "Bulgaria", value: "BG", disabled: false }, + { name: "Burkina Faso", value: "BF", disabled: false }, + { name: "Burundi", value: "BI", disabled: false }, + { name: "Cambodia", value: "KH", disabled: false }, + { name: "Cameroon", value: "CM", disabled: false }, + { name: "Cape Verde", value: "CV", disabled: false }, + { name: "Cayman Islands", value: "KY", disabled: false }, + { name: "Central African Republic", value: "CF", disabled: false }, + { name: "Chad", value: "TD", disabled: false }, + { name: "Chile", value: "CL", disabled: false }, + { name: "Christmas Island", value: "CX", disabled: false }, + { name: "Cocos (Keeling) Islands", value: "CC", disabled: false }, + { name: "Colombia", value: "CO", disabled: false }, + { name: "Comoros", value: "KM", disabled: false }, + { name: "Congo", value: "CG", disabled: false }, + { name: "Congo, the Democratic Republic of the", value: "CD", disabled: false }, + { name: "Cook Islands", value: "CK", disabled: false }, + { name: "Costa Rica", value: "CR", disabled: false }, + { name: "Côte d'Ivoire", value: "CI", disabled: false }, + { name: "Croatia", value: "HR", disabled: false }, + { name: "Cuba", value: "CU", disabled: false }, + { name: "Curaçao", value: "CW", disabled: false }, + { name: "Cyprus", value: "CY", disabled: false }, + { name: "Czech Republic", value: "CZ", disabled: false }, + { name: "Denmark", value: "DK", disabled: false }, + { name: "Djibouti", value: "DJ", disabled: false }, + { name: "Dominica", value: "DM", disabled: false }, + { name: "Dominican Republic", value: "DO", disabled: false }, + { name: "Ecuador", value: "EC", disabled: false }, + { name: "Egypt", value: "EG", disabled: false }, + { name: "El Salvador", value: "SV", disabled: false }, + { name: "Equatorial Guinea", value: "GQ", disabled: false }, + { name: "Eritrea", value: "ER", disabled: false }, + { name: "Estonia", value: "EE", disabled: false }, + { name: "Ethiopia", value: "ET", disabled: false }, + { name: "Falkland Islands (Malvinas)", value: "FK", disabled: false }, + { name: "Faroe Islands", value: "FO", disabled: false }, + { name: "Fiji", value: "FJ", disabled: false }, + { name: "Finland", value: "FI", disabled: false }, + { name: "French Guiana", value: "GF", disabled: false }, + { name: "French Polynesia", value: "PF", disabled: false }, + { name: "French Southern Territories", value: "TF", disabled: false }, + { name: "Gabon", value: "GA", disabled: false }, + { name: "Gambia", value: "GM", disabled: false }, + { name: "Georgia", value: "GE", disabled: false }, + { name: "Ghana", value: "GH", disabled: false }, + { name: "Gibraltar", value: "GI", disabled: false }, + { name: "Greece", value: "GR", disabled: false }, + { name: "Greenland", value: "GL", disabled: false }, + { name: "Grenada", value: "GD", disabled: false }, + { name: "Guadeloupe", value: "GP", disabled: false }, + { name: "Guam", value: "GU", disabled: false }, + { name: "Guatemala", value: "GT", disabled: false }, + { name: "Guernsey", value: "GG", disabled: false }, + { name: "Guinea", value: "GN", disabled: false }, + { name: "Guinea-Bissau", value: "GW", disabled: false }, + { name: "Guyana", value: "GY", disabled: false }, + { name: "Haiti", value: "HT", disabled: false }, + { name: "Heard Island and McDonald Islands", value: "HM", disabled: false }, + { name: "Holy See (Vatican City State)", value: "VA", disabled: false }, + { name: "Honduras", value: "HN", disabled: false }, + { name: "Hong Kong", value: "HK", disabled: false }, + { name: "Hungary", value: "HU", disabled: false }, + { name: "Iceland", value: "IS", disabled: false }, + { name: "Indonesia", value: "ID", disabled: false }, + { name: "Iran, Islamic Republic of", value: "IR", disabled: false }, + { name: "Iraq", value: "IQ", disabled: false }, + { name: "Ireland", value: "IE", disabled: false }, + { name: "Isle of Man", value: "IM", disabled: false }, + { name: "Israel", value: "IL", disabled: false }, + { name: "Italy", value: "IT", disabled: false }, + { name: "Jamaica", value: "JM", disabled: false }, + { name: "Japan", value: "JP", disabled: false }, + { name: "Jersey", value: "JE", disabled: false }, + { name: "Jordan", value: "JO", disabled: false }, + { name: "Kazakhstan", value: "KZ", disabled: false }, + { name: "Kenya", value: "KE", disabled: false }, + { name: "Kiribati", value: "KI", disabled: false }, + { name: "Korea, Democratic People's Republic of", value: "KP", disabled: false }, + { name: "Korea, Republic of", value: "KR", disabled: false }, + { name: "Kuwait", value: "KW", disabled: false }, + { name: "Kyrgyzstan", value: "KG", disabled: false }, + { name: "Lao People's Democratic Republic", value: "LA", disabled: false }, + { name: "Latvia", value: "LV", disabled: false }, + { name: "Lebanon", value: "LB", disabled: false }, + { name: "Lesotho", value: "LS", disabled: false }, + { name: "Liberia", value: "LR", disabled: false }, + { name: "Libya", value: "LY", disabled: false }, + { name: "Liechtenstein", value: "LI", disabled: false }, + { name: "Lithuania", value: "LT", disabled: false }, + { name: "Luxembourg", value: "LU", disabled: false }, + { name: "Macao", value: "MO", disabled: false }, + { name: "Macedonia, the former Yugoslav Republic of", value: "MK", disabled: false }, + { name: "Madagascar", value: "MG", disabled: false }, + { name: "Malawi", value: "MW", disabled: false }, + { name: "Malaysia", value: "MY", disabled: false }, + { name: "Maldives", value: "MV", disabled: false }, + { name: "Mali", value: "ML", disabled: false }, + { name: "Malta", value: "MT", disabled: false }, + { name: "Marshall Islands", value: "MH", disabled: false }, + { name: "Martinique", value: "MQ", disabled: false }, + { name: "Mauritania", value: "MR", disabled: false }, + { name: "Mauritius", value: "MU", disabled: false }, + { name: "Mayotte", value: "YT", disabled: false }, + { name: "Mexico", value: "MX", disabled: false }, + { name: "Micronesia, Federated States of", value: "FM", disabled: false }, + { name: "Moldova, Republic of", value: "MD", disabled: false }, + { name: "Monaco", value: "MC", disabled: false }, + { name: "Mongolia", value: "MN", disabled: false }, + { name: "Montenegro", value: "ME", disabled: false }, + { name: "Montserrat", value: "MS", disabled: false }, + { name: "Morocco", value: "MA", disabled: false }, + { name: "Mozambique", value: "MZ", disabled: false }, + { name: "Myanmar", value: "MM", disabled: false }, + { name: "Namibia", value: "NA", disabled: false }, + { name: "Nauru", value: "NR", disabled: false }, + { name: "Nepal", value: "NP", disabled: false }, + { name: "Netherlands", value: "NL", disabled: false }, + { name: "New Caledonia", value: "NC", disabled: false }, + { name: "New Zealand", value: "NZ", disabled: false }, + { name: "Nicaragua", value: "NI", disabled: false }, + { name: "Niger", value: "NE", disabled: false }, + { name: "Nigeria", value: "NG", disabled: false }, + { name: "Niue", value: "NU", disabled: false }, + { name: "Norfolk Island", value: "NF", disabled: false }, + { name: "Northern Mariana Islands", value: "MP", disabled: false }, + { name: "Norway", value: "NO", disabled: false }, + { name: "Oman", value: "OM", disabled: false }, + { name: "Pakistan", value: "PK", disabled: false }, + { name: "Palau", value: "PW", disabled: false }, + { name: "Palestinian Territory, Occupied", value: "PS", disabled: false }, + { name: "Panama", value: "PA", disabled: false }, + { name: "Papua New Guinea", value: "PG", disabled: false }, + { name: "Paraguay", value: "PY", disabled: false }, + { name: "Peru", value: "PE", disabled: false }, + { name: "Philippines", value: "PH", disabled: false }, + { name: "Pitcairn", value: "PN", disabled: false }, + { name: "Poland", value: "PL", disabled: false }, + { name: "Portugal", value: "PT", disabled: false }, + { name: "Puerto Rico", value: "PR", disabled: false }, + { name: "Qatar", value: "QA", disabled: false }, + { name: "Réunion", value: "RE", disabled: false }, + { name: "Romania", value: "RO", disabled: false }, + { name: "Russian Federation", value: "RU", disabled: false }, + { name: "Rwanda", value: "RW", disabled: false }, + { name: "Saint Barthélemy", value: "BL", disabled: false }, + { name: "Saint Helena, Ascension and Tristan da Cunha", value: "SH", disabled: false }, + { name: "Saint Kitts and Nevis", value: "KN", disabled: false }, + { name: "Saint Lucia", value: "LC", disabled: false }, + { name: "Saint Martin (French part)", value: "MF", disabled: false }, + { name: "Saint Pierre and Miquelon", value: "PM", disabled: false }, + { name: "Saint Vincent and the Grenadines", value: "VC", disabled: false }, + { name: "Samoa", value: "WS", disabled: false }, + { name: "San Marino", value: "SM", disabled: false }, + { name: "Sao Tome and Principe", value: "ST", disabled: false }, + { name: "Saudi Arabia", value: "SA", disabled: false }, + { name: "Senegal", value: "SN", disabled: false }, + { name: "Serbia", value: "RS", disabled: false }, + { name: "Seychelles", value: "SC", disabled: false }, + { name: "Sierra Leone", value: "SL", disabled: false }, + { name: "Singapore", value: "SG", disabled: false }, + { name: "Sint Maarten (Dutch part)", value: "SX", disabled: false }, + { name: "Slovakia", value: "SK", disabled: false }, + { name: "Slovenia", value: "SI", disabled: false }, + { name: "Solomon Islands", value: "SB", disabled: false }, + { name: "Somalia", value: "SO", disabled: false }, + { name: "South Africa", value: "ZA", disabled: false }, + { name: "South Georgia and the South Sandwich Islands", value: "GS", disabled: false }, + { name: "South Sudan", value: "SS", disabled: false }, + { name: "Spain", value: "ES", disabled: false }, + { name: "Sri Lanka", value: "LK", disabled: false }, + { name: "Sudan", value: "SD", disabled: false }, + { name: "Suriname", value: "SR", disabled: false }, + { name: "Svalbard and Jan Mayen", value: "SJ", disabled: false }, + { name: "Swaziland", value: "SZ", disabled: false }, + { name: "Sweden", value: "SE", disabled: false }, + { name: "Switzerland", value: "CH", disabled: false }, + { name: "Syrian Arab Republic", value: "SY", disabled: false }, + { name: "Taiwan", value: "TW", disabled: false }, + { name: "Tajikistan", value: "TJ", disabled: false }, + { name: "Tanzania, United Republic of", value: "TZ", disabled: false }, + { name: "Thailand", value: "TH", disabled: false }, + { name: "Timor-Leste", value: "TL", disabled: false }, + { name: "Togo", value: "TG", disabled: false }, + { name: "Tokelau", value: "TK", disabled: false }, + { name: "Tonga", value: "TO", disabled: false }, + { name: "Trinidad and Tobago", value: "TT", disabled: false }, + { name: "Tunisia", value: "TN", disabled: false }, + { name: "Turkey", value: "TR", disabled: false }, + { name: "Turkmenistan", value: "TM", disabled: false }, + { name: "Turks and Caicos Islands", value: "TC", disabled: false }, + { name: "Tuvalu", value: "TV", disabled: false }, + { name: "Uganda", value: "UG", disabled: false }, + { name: "Ukraine", value: "UA", disabled: false }, + { name: "United Arab Emirates", value: "AE", disabled: false }, + { name: "United States Minor Outlying Islands", value: "UM", disabled: false }, + { name: "Uruguay", value: "UY", disabled: false }, + { name: "Uzbekistan", value: "UZ", disabled: false }, + { name: "Vanuatu", value: "VU", disabled: false }, + { name: "Venezuela, Bolivarian Republic of", value: "VE", disabled: false }, + { name: "Viet Nam", value: "VN", disabled: false }, + { name: "Virgin Islands, British", value: "VG", disabled: false }, + { name: "Virgin Islands, U.S.", value: "VI", disabled: false }, + { name: "Wallis and Futuna", value: "WF", disabled: false }, + { name: "Western Sahara", value: "EH", disabled: false }, + { name: "Yemen", value: "YE", disabled: false }, + { name: "Zambia", value: "ZM", disabled: false }, + { name: "Zimbabwe", value: "ZW", disabled: false }, + ]; + + private taxSupportedCountryCodes: string[] = [ + "CN", + "FR", + "DE", + "CA", + "GB", + "AU", + "IN", + "AD", + "AR", + "AT", + "BE", + "BO", + "BR", + "BG", + "CL", + "CO", + "CR", + "HR", + "CY", + "CZ", + "DK", + "DO", + "EC", + "EG", + "SV", + "EE", + "FI", + "GE", + "GR", + "HK", + "HU", + "IS", + "ID", + "IQ", + "IE", + "IL", + "IT", + "JP", + "KE", + "KR", + "LV", + "LI", + "LT", + "LU", + "MY", + "MT", + "MX", + "NL", + "NZ", + "NO", + "PE", + "PH", + "PL", + "PT", + "RO", + "RU", + "SA", + "RS", + "SG", + "SK", + "SI", + "ZA", + "ES", + "SE", + "CH", + "TW", + "TH", + "TR", + "UA", + "AE", + "UY", + "VE", + "VN", + ]; } diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 4b876db247d..a43f1fa07a8 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -134,13 +134,11 @@ 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 { 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 { OrganizationBillingService } from "@bitwarden/common/billing/services/organization-billing.service"; -import { TaxService } from "@bitwarden/common/billing/services/tax.service"; import { AppIdService as AppIdServiceAbstraction } from "@bitwarden/common/platform/abstractions/app-id.service"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; import { BulkEncryptService } from "@bitwarden/common/platform/abstractions/bulk-encrypt.service"; @@ -1264,11 +1262,6 @@ const safeProviders: SafeProvider[] = [ useClass: BillingApiService, deps: [ApiServiceAbstraction, LogService, ToastService], }), - safeProvider({ - provide: TaxServiceAbstraction, - useClass: TaxService, - deps: [ApiServiceAbstraction], - }), safeProvider({ provide: BillingAccountProfileStateService, useClass: DefaultBillingAccountProfileStateService, diff --git a/libs/common/src/billing/abstractions/tax.service.abstraction.ts b/libs/common/src/billing/abstractions/tax.service.abstraction.ts deleted file mode 100644 index fea4618bc02..00000000000 --- a/libs/common/src/billing/abstractions/tax.service.abstraction.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { CountryListItem } from "@bitwarden/common/billing/models/domain"; -import { PreviewIndividualInvoiceRequest } from "@bitwarden/common/billing/models/request/preview-individual-invoice.request"; -import { PreviewOrganizationInvoiceRequest } from "@bitwarden/common/billing/models/request/preview-organization-invoice.request"; -import { PreviewInvoiceResponse } from "@bitwarden/common/billing/models/response/preview-invoice.response"; - -export abstract class TaxServiceAbstraction { - getCountries: () => CountryListItem[]; - - isCountrySupported: (country: string) => Promise; - - previewIndividualInvoice: ( - request: PreviewIndividualInvoiceRequest, - ) => Promise; - - previewOrganizationInvoice: ( - request: PreviewOrganizationInvoiceRequest, - ) => Promise; -} diff --git a/libs/common/src/billing/models/domain/country-list-item.ts b/libs/common/src/billing/models/domain/country-list-item.ts deleted file mode 100644 index 79abb96871c..00000000000 --- a/libs/common/src/billing/models/domain/country-list-item.ts +++ /dev/null @@ -1,5 +0,0 @@ -export type CountryListItem = { - name: string; - value: string; - disabled: boolean; -}; diff --git a/libs/common/src/billing/models/domain/index.ts b/libs/common/src/billing/models/domain/index.ts index 057f6dc4e84..0f53c3e116c 100644 --- a/libs/common/src/billing/models/domain/index.ts +++ b/libs/common/src/billing/models/domain/index.ts @@ -1,3 +1,2 @@ export * from "./bank-account"; -export * from "./country-list-item"; export * from "./tax-information"; diff --git a/libs/common/src/billing/models/request/expanded-tax-info-update.request.ts b/libs/common/src/billing/models/request/expanded-tax-info-update.request.ts index 0cedc5d8e67..f06795c0805 100644 --- a/libs/common/src/billing/models/request/expanded-tax-info-update.request.ts +++ b/libs/common/src/billing/models/request/expanded-tax-info-update.request.ts @@ -10,10 +10,6 @@ export class ExpandedTaxInfoUpdateRequest extends TaxInfoUpdateRequest { state: string; static From(taxInformation: TaxInformation): ExpandedTaxInfoUpdateRequest { - if (!taxInformation) { - return null; - } - const request = new ExpandedTaxInfoUpdateRequest(); request.country = taxInformation.country; request.postalCode = taxInformation.postalCode; diff --git a/libs/common/src/billing/models/request/preview-individual-invoice.request.ts b/libs/common/src/billing/models/request/preview-individual-invoice.request.ts deleted file mode 100644 index 67b7b2f3ee6..00000000000 --- a/libs/common/src/billing/models/request/preview-individual-invoice.request.ts +++ /dev/null @@ -1,14 +0,0 @@ -export class PreviewIndividualInvoiceRequest { - passwordManager: PasswordManager; - taxInformation: TaxInformation; -} - -class PasswordManager { - additionalStorage: number; -} - -class TaxInformation { - postalCode: string; - country: string; - taxId: string; -} diff --git a/libs/common/src/billing/models/request/preview-organization-invoice.request.ts b/libs/common/src/billing/models/request/preview-organization-invoice.request.ts deleted file mode 100644 index 2fe2526fdce..00000000000 --- a/libs/common/src/billing/models/request/preview-organization-invoice.request.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { PlanType } from "@bitwarden/common/billing/enums"; - -export class PreviewOrganizationInvoiceRequest { - organizationId?: string; - passwordManager: PasswordManager; - secretsManager?: SecretsManager; - taxInformation: TaxInformation; -} - -class PasswordManager { - plan: PlanType; - seats: number; - additionalStorage: number; -} - -class SecretsManager { - seats: number; - additionalMachineAccounts: number; -} - -class TaxInformation { - postalCode: string; - country: string; - taxId: string; -} diff --git a/libs/common/src/billing/models/response/preview-invoice.response.ts b/libs/common/src/billing/models/response/preview-invoice.response.ts deleted file mode 100644 index c822a569bb3..00000000000 --- a/libs/common/src/billing/models/response/preview-invoice.response.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { BaseResponse } from "@bitwarden/common/models/response/base.response"; - -export class PreviewInvoiceResponse extends BaseResponse { - effectiveTaxRate: number; - taxableBaseAmount: number; - taxAmount: number; - totalAmount: number; - - constructor(response: any) { - super(response); - this.effectiveTaxRate = this.getResponseProperty("EffectiveTaxRate"); - this.taxableBaseAmount = this.getResponseProperty("TaxableBaseAmount"); - this.taxAmount = this.getResponseProperty("TaxAmount"); - this.totalAmount = this.getResponseProperty("TotalAmount"); - } -} diff --git a/libs/common/src/billing/models/response/tax-id-types.response.ts b/libs/common/src/billing/models/response/tax-id-types.response.ts deleted file mode 100644 index 0d5cce46c8c..00000000000 --- a/libs/common/src/billing/models/response/tax-id-types.response.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { BaseResponse } from "@bitwarden/common/models/response/base.response"; - -export class TaxIdTypesResponse extends BaseResponse { - taxIdTypes: TaxIdTypeResponse[] = []; - - constructor(response: any) { - super(response); - const taxIdTypes = this.getResponseProperty("TaxIdTypes"); - if (taxIdTypes && taxIdTypes.length) { - this.taxIdTypes = taxIdTypes.map((t: any) => new TaxIdTypeResponse(t)); - } - } -} - -export class TaxIdTypeResponse extends BaseResponse { - code: string; - country: string; - description: string; - example: string; - - constructor(response: any) { - super(response); - this.code = this.getResponseProperty("Code"); - this.country = this.getResponseProperty("Country"); - this.description = this.getResponseProperty("Description"); - this.example = this.getResponseProperty("Example"); - } -} diff --git a/libs/common/src/billing/services/tax.service.ts b/libs/common/src/billing/services/tax.service.ts deleted file mode 100644 index 45e57267ec0..00000000000 --- a/libs/common/src/billing/services/tax.service.ts +++ /dev/null @@ -1,303 +0,0 @@ -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { TaxServiceAbstraction } from "@bitwarden/common/billing/abstractions/tax.service.abstraction"; -import { CountryListItem } from "@bitwarden/common/billing/models/domain"; -import { PreviewIndividualInvoiceRequest } from "@bitwarden/common/billing/models/request/preview-individual-invoice.request"; -import { PreviewOrganizationInvoiceRequest } from "@bitwarden/common/billing/models/request/preview-organization-invoice.request"; -import { PreviewInvoiceResponse } from "@bitwarden/common/billing/models/response/preview-invoice.response"; - -export class TaxService implements TaxServiceAbstraction { - constructor(private apiService: ApiService) {} - - getCountries(): CountryListItem[] { - return [ - { name: "-- Select --", value: "", disabled: false }, - { name: "United States", value: "US", disabled: false }, - { name: "China", value: "CN", disabled: false }, - { name: "France", value: "FR", disabled: false }, - { name: "Germany", value: "DE", disabled: false }, - { name: "Canada", value: "CA", disabled: false }, - { name: "United Kingdom", value: "GB", disabled: false }, - { name: "Australia", value: "AU", disabled: false }, - { name: "India", value: "IN", disabled: false }, - { name: "", value: "-", disabled: true }, - { name: "Afghanistan", value: "AF", disabled: false }, - { name: "Åland Islands", value: "AX", disabled: false }, - { name: "Albania", value: "AL", disabled: false }, - { name: "Algeria", value: "DZ", disabled: false }, - { name: "American Samoa", value: "AS", disabled: false }, - { name: "Andorra", value: "AD", disabled: false }, - { name: "Angola", value: "AO", disabled: false }, - { name: "Anguilla", value: "AI", disabled: false }, - { name: "Antarctica", value: "AQ", disabled: false }, - { name: "Antigua and Barbuda", value: "AG", disabled: false }, - { name: "Argentina", value: "AR", disabled: false }, - { name: "Armenia", value: "AM", disabled: false }, - { name: "Aruba", value: "AW", disabled: false }, - { name: "Austria", value: "AT", disabled: false }, - { name: "Azerbaijan", value: "AZ", disabled: false }, - { name: "Bahamas", value: "BS", disabled: false }, - { name: "Bahrain", value: "BH", disabled: false }, - { name: "Bangladesh", value: "BD", disabled: false }, - { name: "Barbados", value: "BB", disabled: false }, - { name: "Belarus", value: "BY", disabled: false }, - { name: "Belgium", value: "BE", disabled: false }, - { name: "Belize", value: "BZ", disabled: false }, - { name: "Benin", value: "BJ", disabled: false }, - { name: "Bermuda", value: "BM", disabled: false }, - { name: "Bhutan", value: "BT", disabled: false }, - { name: "Bolivia, Plurinational State of", value: "BO", disabled: false }, - { name: "Bonaire, Sint Eustatius and Saba", value: "BQ", disabled: false }, - { name: "Bosnia and Herzegovina", value: "BA", disabled: false }, - { name: "Botswana", value: "BW", disabled: false }, - { name: "Bouvet Island", value: "BV", disabled: false }, - { name: "Brazil", value: "BR", disabled: false }, - { name: "British Indian Ocean Territory", value: "IO", disabled: false }, - { name: "Brunei Darussalam", value: "BN", disabled: false }, - { name: "Bulgaria", value: "BG", disabled: false }, - { name: "Burkina Faso", value: "BF", disabled: false }, - { name: "Burundi", value: "BI", disabled: false }, - { name: "Cambodia", value: "KH", disabled: false }, - { name: "Cameroon", value: "CM", disabled: false }, - { name: "Cape Verde", value: "CV", disabled: false }, - { name: "Cayman Islands", value: "KY", disabled: false }, - { name: "Central African Republic", value: "CF", disabled: false }, - { name: "Chad", value: "TD", disabled: false }, - { name: "Chile", value: "CL", disabled: false }, - { name: "Christmas Island", value: "CX", disabled: false }, - { name: "Cocos (Keeling) Islands", value: "CC", disabled: false }, - { name: "Colombia", value: "CO", disabled: false }, - { name: "Comoros", value: "KM", disabled: false }, - { name: "Congo", value: "CG", disabled: false }, - { name: "Congo, the Democratic Republic of the", value: "CD", disabled: false }, - { name: "Cook Islands", value: "CK", disabled: false }, - { name: "Costa Rica", value: "CR", disabled: false }, - { name: "Côte d'Ivoire", value: "CI", disabled: false }, - { name: "Croatia", value: "HR", disabled: false }, - { name: "Cuba", value: "CU", disabled: false }, - { name: "Curaçao", value: "CW", disabled: false }, - { name: "Cyprus", value: "CY", disabled: false }, - { name: "Czech Republic", value: "CZ", disabled: false }, - { name: "Denmark", value: "DK", disabled: false }, - { name: "Djibouti", value: "DJ", disabled: false }, - { name: "Dominica", value: "DM", disabled: false }, - { name: "Dominican Republic", value: "DO", disabled: false }, - { name: "Ecuador", value: "EC", disabled: false }, - { name: "Egypt", value: "EG", disabled: false }, - { name: "El Salvador", value: "SV", disabled: false }, - { name: "Equatorial Guinea", value: "GQ", disabled: false }, - { name: "Eritrea", value: "ER", disabled: false }, - { name: "Estonia", value: "EE", disabled: false }, - { name: "Ethiopia", value: "ET", disabled: false }, - { name: "Falkland Islands (Malvinas)", value: "FK", disabled: false }, - { name: "Faroe Islands", value: "FO", disabled: false }, - { name: "Fiji", value: "FJ", disabled: false }, - { name: "Finland", value: "FI", disabled: false }, - { name: "French Guiana", value: "GF", disabled: false }, - { name: "French Polynesia", value: "PF", disabled: false }, - { name: "French Southern Territories", value: "TF", disabled: false }, - { name: "Gabon", value: "GA", disabled: false }, - { name: "Gambia", value: "GM", disabled: false }, - { name: "Georgia", value: "GE", disabled: false }, - { name: "Ghana", value: "GH", disabled: false }, - { name: "Gibraltar", value: "GI", disabled: false }, - { name: "Greece", value: "GR", disabled: false }, - { name: "Greenland", value: "GL", disabled: false }, - { name: "Grenada", value: "GD", disabled: false }, - { name: "Guadeloupe", value: "GP", disabled: false }, - { name: "Guam", value: "GU", disabled: false }, - { name: "Guatemala", value: "GT", disabled: false }, - { name: "Guernsey", value: "GG", disabled: false }, - { name: "Guinea", value: "GN", disabled: false }, - { name: "Guinea-Bissau", value: "GW", disabled: false }, - { name: "Guyana", value: "GY", disabled: false }, - { name: "Haiti", value: "HT", disabled: false }, - { name: "Heard Island and McDonald Islands", value: "HM", disabled: false }, - { name: "Holy See (Vatican City State)", value: "VA", disabled: false }, - { name: "Honduras", value: "HN", disabled: false }, - { name: "Hong Kong", value: "HK", disabled: false }, - { name: "Hungary", value: "HU", disabled: false }, - { name: "Iceland", value: "IS", disabled: false }, - { name: "Indonesia", value: "ID", disabled: false }, - { name: "Iran, Islamic Republic of", value: "IR", disabled: false }, - { name: "Iraq", value: "IQ", disabled: false }, - { name: "Ireland", value: "IE", disabled: false }, - { name: "Isle of Man", value: "IM", disabled: false }, - { name: "Israel", value: "IL", disabled: false }, - { name: "Italy", value: "IT", disabled: false }, - { name: "Jamaica", value: "JM", disabled: false }, - { name: "Japan", value: "JP", disabled: false }, - { name: "Jersey", value: "JE", disabled: false }, - { name: "Jordan", value: "JO", disabled: false }, - { name: "Kazakhstan", value: "KZ", disabled: false }, - { name: "Kenya", value: "KE", disabled: false }, - { name: "Kiribati", value: "KI", disabled: false }, - { name: "Korea, Democratic People's Republic of", value: "KP", disabled: false }, - { name: "Korea, Republic of", value: "KR", disabled: false }, - { name: "Kuwait", value: "KW", disabled: false }, - { name: "Kyrgyzstan", value: "KG", disabled: false }, - { name: "Lao People's Democratic Republic", value: "LA", disabled: false }, - { name: "Latvia", value: "LV", disabled: false }, - { name: "Lebanon", value: "LB", disabled: false }, - { name: "Lesotho", value: "LS", disabled: false }, - { name: "Liberia", value: "LR", disabled: false }, - { name: "Libya", value: "LY", disabled: false }, - { name: "Liechtenstein", value: "LI", disabled: false }, - { name: "Lithuania", value: "LT", disabled: false }, - { name: "Luxembourg", value: "LU", disabled: false }, - { name: "Macao", value: "MO", disabled: false }, - { name: "Macedonia, the former Yugoslav Republic of", value: "MK", disabled: false }, - { name: "Madagascar", value: "MG", disabled: false }, - { name: "Malawi", value: "MW", disabled: false }, - { name: "Malaysia", value: "MY", disabled: false }, - { name: "Maldives", value: "MV", disabled: false }, - { name: "Mali", value: "ML", disabled: false }, - { name: "Malta", value: "MT", disabled: false }, - { name: "Marshall Islands", value: "MH", disabled: false }, - { name: "Martinique", value: "MQ", disabled: false }, - { name: "Mauritania", value: "MR", disabled: false }, - { name: "Mauritius", value: "MU", disabled: false }, - { name: "Mayotte", value: "YT", disabled: false }, - { name: "Mexico", value: "MX", disabled: false }, - { name: "Micronesia, Federated States of", value: "FM", disabled: false }, - { name: "Moldova, Republic of", value: "MD", disabled: false }, - { name: "Monaco", value: "MC", disabled: false }, - { name: "Mongolia", value: "MN", disabled: false }, - { name: "Montenegro", value: "ME", disabled: false }, - { name: "Montserrat", value: "MS", disabled: false }, - { name: "Morocco", value: "MA", disabled: false }, - { name: "Mozambique", value: "MZ", disabled: false }, - { name: "Myanmar", value: "MM", disabled: false }, - { name: "Namibia", value: "NA", disabled: false }, - { name: "Nauru", value: "NR", disabled: false }, - { name: "Nepal", value: "NP", disabled: false }, - { name: "Netherlands", value: "NL", disabled: false }, - { name: "New Caledonia", value: "NC", disabled: false }, - { name: "New Zealand", value: "NZ", disabled: false }, - { name: "Nicaragua", value: "NI", disabled: false }, - { name: "Niger", value: "NE", disabled: false }, - { name: "Nigeria", value: "NG", disabled: false }, - { name: "Niue", value: "NU", disabled: false }, - { name: "Norfolk Island", value: "NF", disabled: false }, - { name: "Northern Mariana Islands", value: "MP", disabled: false }, - { name: "Norway", value: "NO", disabled: false }, - { name: "Oman", value: "OM", disabled: false }, - { name: "Pakistan", value: "PK", disabled: false }, - { name: "Palau", value: "PW", disabled: false }, - { name: "Palestinian Territory, Occupied", value: "PS", disabled: false }, - { name: "Panama", value: "PA", disabled: false }, - { name: "Papua New Guinea", value: "PG", disabled: false }, - { name: "Paraguay", value: "PY", disabled: false }, - { name: "Peru", value: "PE", disabled: false }, - { name: "Philippines", value: "PH", disabled: false }, - { name: "Pitcairn", value: "PN", disabled: false }, - { name: "Poland", value: "PL", disabled: false }, - { name: "Portugal", value: "PT", disabled: false }, - { name: "Puerto Rico", value: "PR", disabled: false }, - { name: "Qatar", value: "QA", disabled: false }, - { name: "Réunion", value: "RE", disabled: false }, - { name: "Romania", value: "RO", disabled: false }, - { name: "Russian Federation", value: "RU", disabled: false }, - { name: "Rwanda", value: "RW", disabled: false }, - { name: "Saint Barthélemy", value: "BL", disabled: false }, - { name: "Saint Helena, Ascension and Tristan da Cunha", value: "SH", disabled: false }, - { name: "Saint Kitts and Nevis", value: "KN", disabled: false }, - { name: "Saint Lucia", value: "LC", disabled: false }, - { name: "Saint Martin (French part)", value: "MF", disabled: false }, - { name: "Saint Pierre and Miquelon", value: "PM", disabled: false }, - { name: "Saint Vincent and the Grenadines", value: "VC", disabled: false }, - { name: "Samoa", value: "WS", disabled: false }, - { name: "San Marino", value: "SM", disabled: false }, - { name: "Sao Tome and Principe", value: "ST", disabled: false }, - { name: "Saudi Arabia", value: "SA", disabled: false }, - { name: "Senegal", value: "SN", disabled: false }, - { name: "Serbia", value: "RS", disabled: false }, - { name: "Seychelles", value: "SC", disabled: false }, - { name: "Sierra Leone", value: "SL", disabled: false }, - { name: "Singapore", value: "SG", disabled: false }, - { name: "Sint Maarten (Dutch part)", value: "SX", disabled: false }, - { name: "Slovakia", value: "SK", disabled: false }, - { name: "Slovenia", value: "SI", disabled: false }, - { name: "Solomon Islands", value: "SB", disabled: false }, - { name: "Somalia", value: "SO", disabled: false }, - { name: "South Africa", value: "ZA", disabled: false }, - { name: "South Georgia and the South Sandwich Islands", value: "GS", disabled: false }, - { name: "South Sudan", value: "SS", disabled: false }, - { name: "Spain", value: "ES", disabled: false }, - { name: "Sri Lanka", value: "LK", disabled: false }, - { name: "Sudan", value: "SD", disabled: false }, - { name: "Suriname", value: "SR", disabled: false }, - { name: "Svalbard and Jan Mayen", value: "SJ", disabled: false }, - { name: "Swaziland", value: "SZ", disabled: false }, - { name: "Sweden", value: "SE", disabled: false }, - { name: "Switzerland", value: "CH", disabled: false }, - { name: "Syrian Arab Republic", value: "SY", disabled: false }, - { name: "Taiwan", value: "TW", disabled: false }, - { name: "Tajikistan", value: "TJ", disabled: false }, - { name: "Tanzania, United Republic of", value: "TZ", disabled: false }, - { name: "Thailand", value: "TH", disabled: false }, - { name: "Timor-Leste", value: "TL", disabled: false }, - { name: "Togo", value: "TG", disabled: false }, - { name: "Tokelau", value: "TK", disabled: false }, - { name: "Tonga", value: "TO", disabled: false }, - { name: "Trinidad and Tobago", value: "TT", disabled: false }, - { name: "Tunisia", value: "TN", disabled: false }, - { name: "Turkey", value: "TR", disabled: false }, - { name: "Turkmenistan", value: "TM", disabled: false }, - { name: "Turks and Caicos Islands", value: "TC", disabled: false }, - { name: "Tuvalu", value: "TV", disabled: false }, - { name: "Uganda", value: "UG", disabled: false }, - { name: "Ukraine", value: "UA", disabled: false }, - { name: "United Arab Emirates", value: "AE", disabled: false }, - { name: "United States Minor Outlying Islands", value: "UM", disabled: false }, - { name: "Uruguay", value: "UY", disabled: false }, - { name: "Uzbekistan", value: "UZ", disabled: false }, - { name: "Vanuatu", value: "VU", disabled: false }, - { name: "Venezuela, Bolivarian Republic of", value: "VE", disabled: false }, - { name: "Viet Nam", value: "VN", disabled: false }, - { name: "Virgin Islands, British", value: "VG", disabled: false }, - { name: "Virgin Islands, U.S.", value: "VI", disabled: false }, - { name: "Wallis and Futuna", value: "WF", disabled: false }, - { name: "Western Sahara", value: "EH", disabled: false }, - { name: "Yemen", value: "YE", disabled: false }, - { name: "Zambia", value: "ZM", disabled: false }, - { name: "Zimbabwe", value: "ZW", disabled: false }, - ]; - } - - async isCountrySupported(country: string): Promise { - const response = await this.apiService.send( - "GET", - "/tax/is-country-supported?country=" + country, - null, - true, - true, - ); - return response; - } - - async previewIndividualInvoice( - request: PreviewIndividualInvoiceRequest, - ): Promise { - const response = await this.apiService.send( - "POST", - "/accounts/billing/preview-invoice", - request, - true, - true, - ); - return new PreviewInvoiceResponse(response); - } - - async previewOrganizationInvoice( - request: PreviewOrganizationInvoiceRequest, - ): Promise { - const response = await this.apiService.send( - "POST", - `/invoices/preview-organization`, - request, - true, - true, - ); - return new PreviewInvoiceResponse(response); - } -} From 98702d9f5073a594b74ab27d5998d497cc6c8294 Mon Sep 17 00:00:00 2001 From: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> Date: Wed, 4 Dec 2024 15:59:06 +0100 Subject: [PATCH 21/46] Fix the new send dropdown showing premium when it should not (#12242) Co-authored-by: Daniel James Smith --- .../src/new-send-dropdown/new-send-dropdown.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 9a65f7d98c5..344348f6a90 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 @@ -5,7 +5,7 @@ From 80a898bd8c7ca14cd7e9d61723342ff1b92d4be9 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Wed, 4 Dec 2024 17:03:34 +0100 Subject: [PATCH 22/46] [PM-14252] Switch to oo7 and drop libsecret (#11900) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Switch to oo7 and drop libsecret * Fix tests * Fix windows * Fix windows * Fix windows * Fix windows * Add migration * Update apps/desktop/desktop_native/core/src/password/unix.rs Co-authored-by: Daniel García * Remove libsecret in ci * Move allow async to trait level * Fix comment * Pin oo7 dependency --------- Co-authored-by: Matt Bishop Co-authored-by: Daniel García --- .github/workflows/build-desktop.yml | 2 +- .github/workflows/release-desktop-beta.yml | 2 +- apps/desktop/desktop_native/Cargo.lock | 312 +++++++----------- apps/desktop/desktop_native/core/Cargo.toml | 20 +- .../core/src/biometric/macos.rs | 4 +- .../desktop_native/core/src/biometric/mod.rs | 7 +- .../desktop_native/core/src/biometric/unix.rs | 8 +- .../core/src/biometric/windows.rs | 42 ++- .../desktop_native/core/src/password/macos.rs | 31 +- .../desktop_native/core/src/password/unix.rs | 154 ++++----- .../core/src/password/windows.rs | 69 +--- apps/desktop/desktop_native/napi/index.d.ts | 2 - apps/desktop/desktop_native/napi/src/lib.rs | 17 +- apps/desktop/electron-builder.json | 2 +- 14 files changed, 278 insertions(+), 394 deletions(-) diff --git a/.github/workflows/build-desktop.yml b/.github/workflows/build-desktop.yml index dc15f841c2b..bc9bdec396a 100644 --- a/.github/workflows/build-desktop.yml +++ b/.github/workflows/build-desktop.yml @@ -170,7 +170,7 @@ jobs: - name: Set up environment run: | sudo apt-get update - sudo apt-get -y install pkg-config libxss-dev libsecret-1-dev rpm musl-dev musl-tools flatpak flatpak-builder + sudo apt-get -y install pkg-config libxss-dev rpm musl-dev musl-tools flatpak flatpak-builder - name: Set up Snap run: sudo snap install snapcraft --classic diff --git a/.github/workflows/release-desktop-beta.yml b/.github/workflows/release-desktop-beta.yml index c1646997201..a940ce289ff 100644 --- a/.github/workflows/release-desktop-beta.yml +++ b/.github/workflows/release-desktop-beta.yml @@ -138,7 +138,7 @@ jobs: - name: Set up environment run: | sudo apt-get update - sudo apt-get -y install pkg-config libxss-dev libsecret-1-dev rpm + sudo apt-get -y install pkg-config libxss-dev rpm - name: Set up Snap run: sudo snap install snapcraft --classic diff --git a/apps/desktop/desktop_native/Cargo.lock b/apps/desktop/desktop_native/Cargo.lock index 5e6a697c6cb..82dbebb12df 100644 --- a/apps/desktop/desktop_native/Cargo.lock +++ b/apps/desktop/desktop_native/Cargo.lock @@ -36,6 +36,7 @@ dependencies = [ "cfg-if", "cipher", "cpufeatures", + "zeroize", ] [[package]] @@ -174,6 +175,17 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "async-net" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7" +dependencies = [ + "async-io", + "blocking", + "futures-lite", +] + [[package]] name = "async-process" version = "2.3.0" @@ -422,16 +434,6 @@ dependencies = [ "shlex", ] -[[package]] -name = "cfg-expr" -version = "0.15.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" -dependencies = [ - "smallvec", - "target-lexicon", -] - [[package]] name = "cfg-if" version = "1.0.0" @@ -469,6 +471,7 @@ checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ "crypto-common", "inout", + "zeroize", ] [[package]] @@ -552,6 +555,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", + "rand_core", "typenum", ] @@ -692,13 +696,12 @@ dependencies = [ "dirs", "ed25519", "futures", - "gio", "homedir", "interprocess", "keytar", "libc", - "libsecret", "log", + "oo7", "pin-project", "pkcs8", "rand", @@ -1079,105 +1082,12 @@ version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" -[[package]] -name = "gio" -version = "0.19.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be548be810e45dd31d3bbb89c6210980bb7af9bca3ea1292b5f16b75f8e394a7" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-util", - "gio-sys", - "glib", - "libc", - "pin-project-lite", - "smallvec", - "thiserror", -] - -[[package]] -name = "gio-sys" -version = "0.19.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cd743ba4714d671ad6b6234e8ab2a13b42304d0e13ab7eba1dcdd78a7d6d4ef" -dependencies = [ - "glib-sys", - "gobject-sys", - "libc", - "system-deps", - "windows-sys 0.52.0", -] - -[[package]] -name = "glib" -version = "0.19.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39650279f135469465018daae0ba53357942a5212137515777d5fdca74984a44" -dependencies = [ - "bitflags", - "futures-channel", - "futures-core", - "futures-executor", - "futures-task", - "futures-util", - "gio-sys", - "glib-macros", - "glib-sys", - "gobject-sys", - "libc", - "memchr", - "smallvec", - "thiserror", -] - -[[package]] -name = "glib-macros" -version = "0.19.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4429b0277a14ae9751350ad9b658b1be0abb5b54faa5bcdf6e74a3372582fad7" -dependencies = [ - "heck", - "proc-macro-crate", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "glib-sys" -version = "0.19.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c2dc18d3a82b0006d470b13304fbbb3e0a9bd4884cf985a60a7ed733ac2c4a5" -dependencies = [ - "libc", - "system-deps", -] - -[[package]] -name = "gobject-sys" -version = "0.19.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e697e252d6e0416fd1d9e169bda51c0f1c926026c39ca21fbe8b1bb5c3b8b9e" -dependencies = [ - "glib-sys", - "libc", - "system-deps", -] - [[package]] name = "hashbrown" version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a9bfc1af68b1726ea47d3d5109de126281def866b33970e10fbab11b5dafab3" -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - [[package]] name = "hermit-abi" version = "0.3.9" @@ -1196,6 +1106,15 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + [[package]] name = "hmac" version = "0.12.1" @@ -1306,9 +1225,9 @@ dependencies = [ [[package]] name = "libm" -version = "0.2.8" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" +checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" [[package]] name = "libredox" @@ -1320,32 +1239,6 @@ dependencies = [ "libc", ] -[[package]] -name = "libsecret" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50c6ccddc706a38eca477b4d7857acd6c76c7d6fba5d47b4b2e7d800e5a17194" -dependencies = [ - "gio", - "glib", - "libc", - "libsecret-sys", -] - -[[package]] -name = "libsecret-sys" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a1af48e61f1c8e77e9705296f346e45b637754a92348a79b4c62df84d0654c2" -dependencies = [ - "gio-sys", - "glib-sys", - "gobject-sys", - "libc", - "pkg-config", - "system-deps", -] - [[package]] name = "link-cplusplus" version = "1.0.9" @@ -1377,6 +1270,16 @@ version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + [[package]] name = "memchr" version = "2.7.4" @@ -1512,6 +1415,30 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + [[package]] name = "num-bigint-dig" version = "0.8.4" @@ -1525,10 +1452,20 @@ dependencies = [ "num-iter", "num-traits", "rand", + "serde", "smallvec", "zeroize", ] +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + [[package]] name = "num-conv" version = "0.1.0" @@ -1555,6 +1492,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -1688,6 +1636,39 @@ version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +[[package]] +name = "oo7" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc6ce4692fbfd044ce22ca07dcab1a30fa12432ca2aa5b1294eca50d3332a24" +dependencies = [ + "aes", + "async-fs", + "async-io", + "async-lock", + "async-net", + "blocking", + "cbc", + "cipher", + "digest", + "endi", + "futures-lite", + "futures-util", + "hkdf", + "hmac", + "md-5", + "num", + "num-bigint-dig", + "pbkdf2", + "rand", + "serde", + "sha2", + "subtle", + "zbus", + "zeroize", + "zvariant", +] + [[package]] name = "opaque-debug" version = "0.3.1" @@ -2216,15 +2197,6 @@ dependencies = [ "syn", ] -[[package]] -name = "serde_spanned" -version = "0.6.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" -dependencies = [ - "serde", -] - [[package]] name = "sha1" version = "0.10.6" @@ -2394,25 +2366,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "system-deps" -version = "6.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" -dependencies = [ - "cfg-expr", - "heck", - "pkg-config", - "toml", - "version-compare", -] - -[[package]] -name = "target-lexicon" -version = "0.12.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" - [[package]] name = "tempfile" version = "3.13.0" @@ -2539,26 +2492,11 @@ dependencies = [ "tokio", ] -[[package]] -name = "toml" -version = "0.8.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" -dependencies = [ - "serde", - "serde_spanned", - "toml_datetime", - "toml_edit", -] - [[package]] name = "toml_datetime" version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" -dependencies = [ - "serde", -] [[package]] name = "toml_edit" @@ -2567,8 +2505,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" dependencies = [ "indexmap", - "serde", - "serde_spanned", "toml_datetime", "winnow", ] @@ -2662,12 +2598,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "version-compare" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" - [[package]] name = "version_check" version = "0.9.5" @@ -3232,6 +3162,20 @@ name = "zeroize" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "zvariant" diff --git a/apps/desktop/desktop_native/core/Cargo.toml b/apps/desktop/desktop_native/core/Cargo.toml index f9b58b7a27d..6e17cde2fa7 100644 --- a/apps/desktop/desktop_native/core/Cargo.toml +++ b/apps/desktop/desktop_native/core/Cargo.toml @@ -10,15 +10,13 @@ default = ["sys"] manual_test = [] sys = [ - "dep:widestring", - "dep:windows", - "dep:core-foundation", - "dep:security-framework", - "dep:security-framework-sys", - "dep:gio", - "dep:libsecret", - "dep:zbus", - "dep:zbus_polkit", + "dep:widestring", + "dep:windows", + "dep:core-foundation", + "dep:security-framework", + "dep:security-framework-sys", + "dep:zbus", + "dep:zbus_polkit", ] [dependencies] @@ -85,7 +83,7 @@ security-framework = { version = "=3.0.0", optional = true } security-framework-sys = { version = "=2.12.0", optional = true } [target.'cfg(target_os = "linux")'.dependencies] -gio = { version = "=0.19.5", optional = true } -libsecret = { version = "=0.5.0", optional = true } +oo7 = "=0.3.3" + zbus = { version = "=4.4.0", optional = true } zbus_polkit = { version = "=4.0.0", optional = true } diff --git a/apps/desktop/desktop_native/core/src/biometric/macos.rs b/apps/desktop/desktop_native/core/src/biometric/macos.rs index 01ee4519ce6..ec09d566e1f 100644 --- a/apps/desktop/desktop_native/core/src/biometric/macos.rs +++ b/apps/desktop/desktop_native/core/src/biometric/macos.rs @@ -18,7 +18,7 @@ impl super::BiometricTrait for Biometric { bail!("platform not supported"); } - fn get_biometric_secret( + async fn get_biometric_secret( _service: &str, _account: &str, _key_material: Option, @@ -26,7 +26,7 @@ impl super::BiometricTrait for Biometric { bail!("platform not supported"); } - fn set_biometric_secret( + async fn set_biometric_secret( _service: &str, _account: &str, _secret: &str, diff --git a/apps/desktop/desktop_native/core/src/biometric/mod.rs b/apps/desktop/desktop_native/core/src/biometric/mod.rs index 72352cf2288..dd480f817b6 100644 --- a/apps/desktop/desktop_native/core/src/biometric/mod.rs +++ b/apps/desktop/desktop_native/core/src/biometric/mod.rs @@ -22,20 +22,19 @@ pub struct OsDerivedKey { pub iv_b64: String, } +#[allow(async_fn_in_trait)] pub trait BiometricTrait { - #[allow(async_fn_in_trait)] async fn prompt(hwnd: Vec, message: String) -> Result; - #[allow(async_fn_in_trait)] async fn available() -> Result; fn derive_key_material(secret: Option<&str>) -> Result; - fn set_biometric_secret( + async fn set_biometric_secret( service: &str, account: &str, secret: &str, key_material: Option, iv_b64: &str, ) -> Result; - fn get_biometric_secret( + async fn get_biometric_secret( service: &str, account: &str, key_material: Option, diff --git a/apps/desktop/desktop_native/core/src/biometric/unix.rs b/apps/desktop/desktop_native/core/src/biometric/unix.rs index 563bd1dfe52..5153fc5ed87 100644 --- a/apps/desktop/desktop_native/core/src/biometric/unix.rs +++ b/apps/desktop/desktop_native/core/src/biometric/unix.rs @@ -73,7 +73,7 @@ impl super::BiometricTrait for Biometric { Ok(OsDerivedKey { key_b64, iv_b64 }) } - fn set_biometric_secret( + async fn set_biometric_secret( service: &str, account: &str, secret: &str, @@ -85,11 +85,11 @@ impl super::BiometricTrait for Biometric { ))?; let encrypted_secret = encrypt(secret, &key_material, iv_b64)?; - crate::password::set_password(service, account, &encrypted_secret)?; + crate::password::set_password(service, account, &encrypted_secret).await?; Ok(encrypted_secret) } - fn get_biometric_secret( + async fn get_biometric_secret( service: &str, account: &str, key_material: Option, @@ -98,7 +98,7 @@ impl super::BiometricTrait for Biometric { "Key material is required for polkit protected keys" ))?; - let encrypted_secret = crate::password::get_password(service, account)?; + let encrypted_secret = crate::password::get_password(service, account).await?; let secret = CipherString::from_str(&encrypted_secret)?; return Ok(decrypt(&secret, &key_material)?); } diff --git a/apps/desktop/desktop_native/core/src/biometric/windows.rs b/apps/desktop/desktop_native/core/src/biometric/windows.rs index c951e42e260..fcc5b95cc4a 100644 --- a/apps/desktop/desktop_native/core/src/biometric/windows.rs +++ b/apps/desktop/desktop_native/core/src/biometric/windows.rs @@ -121,7 +121,7 @@ impl super::BiometricTrait for Biometric { Ok(OsDerivedKey { key_b64, iv_b64 }) } - fn set_biometric_secret( + async fn set_biometric_secret( service: &str, account: &str, secret: &str, @@ -133,11 +133,11 @@ impl super::BiometricTrait for Biometric { ))?; let encrypted_secret = encrypt(secret, &key_material, iv_b64)?; - crate::password::set_password(service, account, &encrypted_secret)?; + crate::password::set_password(service, account, &encrypted_secret).await?; Ok(encrypted_secret) } - fn get_biometric_secret( + async fn get_biometric_secret( service: &str, account: &str, key_material: Option, @@ -146,7 +146,7 @@ impl super::BiometricTrait for Biometric { "Key material is required for Windows Hello protected keys" ))?; - let encrypted_secret = crate::password::get_password(service, account)?; + let encrypted_secret = crate::password::get_password(service, account).await?; match CipherString::from_str(&encrypted_secret) { Ok(secret) => { // If the secret is a CipherString, it is encrypted and we need to decrypt it. @@ -292,9 +292,9 @@ mod tests { assert_eq!(decrypt(&secret, &key_material).unwrap(), "secret") } - #[test] - fn get_biometric_secret_requires_key() { - let result = ::get_biometric_secret("", "", None); + #[tokio::test] + async fn get_biometric_secret_requires_key() { + let result = ::get_biometric_secret("", "", None).await; assert!(result.is_err()); assert_eq!( result.unwrap_err().to_string(), @@ -302,29 +302,25 @@ mod tests { ); } - #[test] - fn get_biometric_secret_handles_unencrypted_secret() { - scopeguard::defer! { - crate::password::delete_password("test", "test").unwrap(); - } + #[tokio::test] + async fn get_biometric_secret_handles_unencrypted_secret() { let test = "test"; let secret = "password"; let key_material = KeyMaterial { os_key_part_b64: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=".to_owned(), client_key_part_b64: Some("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=".to_owned()), }; - crate::password::set_password(test, test, secret).unwrap(); + crate::password::set_password(test, test, secret).await.unwrap(); let result = ::get_biometric_secret(test, test, Some(key_material)) + .await .unwrap(); + crate::password::delete_password("test", "test").await.unwrap(); assert_eq!(result, secret); } - #[test] - fn get_biometric_secret_handles_encrypted_secret() { - scopeguard::defer! { - crate::password::delete_password("test", "test").unwrap(); - } + #[tokio::test] + async fn get_biometric_secret_handles_encrypted_secret() { let test = "test"; let secret = CipherString::from_str("0.l9fhDUP/wDJcKwmEzcb/3w==|uP4LcqoCCj5FxBDP77NV6Q==").unwrap(); // output from test_encrypt @@ -332,17 +328,19 @@ mod tests { os_key_part_b64: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=".to_owned(), client_key_part_b64: Some("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=".to_owned()), }; - crate::password::set_password(test, test, &secret.to_string()).unwrap(); + crate::password::set_password(test, test, &secret.to_string()).await.unwrap(); let result = ::get_biometric_secret(test, test, Some(key_material)) + .await .unwrap(); + crate::password::delete_password("test", "test").await.unwrap(); assert_eq!(result, "secret"); } - #[test] - fn set_biometric_secret_requires_key() { - let result = ::set_biometric_secret("", "", "", None, ""); + #[tokio::test] + async fn set_biometric_secret_requires_key() { + let result = ::set_biometric_secret("", "", "", None, "").await; assert!(result.is_err()); assert_eq!( result.unwrap_err().to_string(), diff --git a/apps/desktop/desktop_native/core/src/password/macos.rs b/apps/desktop/desktop_native/core/src/password/macos.rs index 408706423e2..c911a0d2430 100644 --- a/apps/desktop/desktop_native/core/src/password/macos.rs +++ b/apps/desktop/desktop_native/core/src/password/macos.rs @@ -3,26 +3,22 @@ use security_framework::passwords::{ delete_generic_password, get_generic_password, set_generic_password, }; -pub fn get_password(service: &str, account: &str) -> Result { +pub async fn get_password(service: &str, account: &str) -> Result { let result = String::from_utf8(get_generic_password(&service, &account)?)?; Ok(result) } -pub fn get_password_keytar(service: &str, account: &str) -> Result { - get_password(service, account) -} - -pub fn set_password(service: &str, account: &str, password: &str) -> Result<()> { +pub async fn set_password(service: &str, account: &str, password: &str) -> Result<()> { let result = set_generic_password(&service, &account, password.as_bytes())?; Ok(result) } -pub fn delete_password(service: &str, account: &str) -> Result<()> { +pub async fn delete_password(service: &str, account: &str) -> Result<()> { let result = delete_generic_password(&service, &account)?; Ok(result) } -pub fn is_available() -> Result { +pub async fn is_available() -> Result { Ok(true) } @@ -30,18 +26,17 @@ pub fn is_available() -> Result { mod tests { use super::*; - #[test] - fn test() { - scopeguard::defer!(delete_password("BitwardenTest", "BitwardenTest").unwrap_or({});); - set_password("BitwardenTest", "BitwardenTest", "Random").unwrap(); + #[tokio::test] + async fn test() { + set_password("BitwardenTest", "BitwardenTest", "Random").await.unwrap(); assert_eq!( "Random", - get_password("BitwardenTest", "BitwardenTest").unwrap() + get_password("BitwardenTest", "BitwardenTest").await.unwrap() ); - delete_password("BitwardenTest", "BitwardenTest").unwrap(); + delete_password("BitwardenTest", "BitwardenTest").await.unwrap(); // Ensure password is deleted - match get_password("BitwardenTest", "BitwardenTest") { + match get_password("BitwardenTest", "BitwardenTest").await { Ok(_) => panic!("Got a result"), Err(e) => assert_eq!( "The specified item could not be found in the keychain.", @@ -50,9 +45,9 @@ mod tests { } } - #[test] - fn test_error_no_password() { - match get_password("Unknown", "Unknown") { + #[tokio::test] + async fn test_error_no_password() { + match get_password("Unknown", "Unknown").await { Ok(_) => panic!("Got a result"), Err(e) => assert_eq!( "The specified item could not be found in the keychain.", diff --git a/apps/desktop/desktop_native/core/src/password/unix.rs b/apps/desktop/desktop_native/core/src/password/unix.rs index 1817a4d62ee..20a79625efb 100644 --- a/apps/desktop/desktop_native/core/src/password/unix.rs +++ b/apps/desktop/desktop_native/core/src/password/unix.rs @@ -1,106 +1,106 @@ use anyhow::{anyhow, Result}; -use libsecret::{password_clear_sync, password_lookup_sync, password_store_sync, Schema}; +use oo7::dbus::{self}; use std::collections::HashMap; -pub fn get_password(service: &str, account: &str) -> Result { - let res = password_lookup_sync( - Some(&get_schema()), - build_attributes(service, account), - gio::Cancellable::NONE, - )?; - - match res { - Some(s) => Ok(String::from(s)), - None => Err(anyhow!("No password found")), - } -} - -pub fn get_password_keytar(service: &str, account: &str) -> Result { - get_password(service, account) -} - -pub fn set_password(service: &str, account: &str, password: &str) -> Result<()> { - let result = password_store_sync( - Some(&get_schema()), - build_attributes(service, account), - Some(&libsecret::COLLECTION_DEFAULT), - &format!("{}/{}", service, account), - password, - gio::Cancellable::NONE, - )?; - Ok(result) -} - -pub fn delete_password(service: &str, account: &str) -> Result<()> { - let result = password_clear_sync( - Some(&get_schema()), - build_attributes(service, account), - gio::Cancellable::NONE, - )?; - Ok(result) -} - -pub fn is_available() -> Result { - let result = password_clear_sync( - Some(&get_schema()), - build_attributes("bitwardenSecretsAvailabilityTest", "test"), - gio::Cancellable::NONE, - ); - match result { - Ok(_) => Ok(true), +pub async fn get_password(service: &str, account: &str) -> Result { + match get_password_new(service, account).await { + Ok(res) => Ok(res), Err(_) => { - println!("secret-service unavailable: {:?}", result); - Ok(false) + get_password_legacy(service, account).await } } } -fn get_schema() -> Schema { - let mut attributes = std::collections::HashMap::new(); - attributes.insert("service", libsecret::SchemaAttributeType::String); - attributes.insert("account", libsecret::SchemaAttributeType::String); - - libsecret::Schema::new( - "org.freedesktop.Secret.Generic", - libsecret::SchemaFlags::NONE, - attributes, - ) +async fn get_password_new(service: &str, account: &str) -> Result { + let keyring = oo7::Keyring::new().await?; + let attributes = HashMap::from([("service", service), ("account", account)]); + let results = keyring.search_items(&attributes).await?; + let res = results.get(0); + match res { + Some(res) => { + let secret = res.secret().await?; + Ok(String::from_utf8(secret.to_vec())?) + }, + None => Err(anyhow!("no result")) + } } -fn build_attributes<'a>(service: &'a str, account: &'a str) -> HashMap<&'a str, &'a str> { - let mut attributes = HashMap::new(); - attributes.insert("service", service); - attributes.insert("account", account); +// forces to read via secret service; remvove after 2025.03 +async fn get_password_legacy(service: &str, account: &str) -> Result { + println!("falling back to get legacy {} {}", service, account); + let svc = dbus::Service::new().await?; + let collection = svc.default_collection().await?; + let keyring = oo7::Keyring::DBus(collection); + let attributes = HashMap::from([("service", service), ("account", account)]); + let results = keyring.search_items(&attributes).await?; + let res = results.get(0); + match res { + Some(res) => { + let secret = res.secret().await?; + println!("deleting legacy secret service entry {} {}", service, account); + keyring.delete(&attributes).await?; + let secret_string = String::from_utf8(secret.to_vec())?; + set_password(service, account, &secret_string).await?; + Ok(secret_string) + }, + None => Err(anyhow!("no result")) + } +} - attributes +pub async fn set_password(service: &str, account: &str, password: &str) -> Result<()> { + let keyring = oo7::Keyring::new().await?; + let attributes = HashMap::from([("service", service), ("account", account)]); + keyring.create_item("org.freedesktop.Secret.Generic", &attributes, password, true).await?; + Ok(()) +} + +pub async fn delete_password(service: &str, account: &str) -> Result<()> { + let keyring = oo7::Keyring::new().await?; + let attributes = HashMap::from([("service", service), ("account", account)]); + keyring.delete(&attributes).await?; + Ok(()) +} + +pub async fn is_available() -> Result { + match oo7::Keyring::new().await { + Ok(_) => Ok(true), + _ => Ok(false), + } } #[cfg(test)] mod tests { use super::*; - #[test] - fn test() { - scopeguard::defer!(delete_password("BitwardenTest", "BitwardenTest").unwrap_or({});); - set_password("BitwardenTest", "BitwardenTest", "Random").unwrap(); + #[tokio::test] + async fn test() { + set_password("BitwardenTest", "BitwardenTest", "Random").await.unwrap(); assert_eq!( "Random", - get_password("BitwardenTest", "BitwardenTest").unwrap() + get_password("BitwardenTest", "BitwardenTest").await.unwrap() ); - delete_password("BitwardenTest", "BitwardenTest").unwrap(); + delete_password("BitwardenTest", "BitwardenTest").await.unwrap(); // Ensure password is deleted - match get_password("BitwardenTest", "BitwardenTest") { - Ok(_) => panic!("Got a result"), - Err(e) => assert_eq!("No password found", e.to_string()), + match get_password("BitwardenTest", "BitwardenTest").await { + Ok(_) => { + panic!("Got a result") + } + Err(e) => assert_eq!( + "no result", + e.to_string() + ), } } - #[test] - fn test_error_no_password() { - match get_password("BitwardenTest", "BitwardenTest") { + #[tokio::test] + async fn test_error_no_password() { + match get_password("Unknown", "Unknown").await { Ok(_) => panic!("Got a result"), - Err(e) => assert_eq!("No password found", e.to_string()), + Err(e) => assert_eq!( + "no result", + e.to_string() + ), } } } diff --git a/apps/desktop/desktop_native/core/src/password/windows.rs b/apps/desktop/desktop_native/core/src/password/windows.rs index d932aabae95..873e717ac8b 100644 --- a/apps/desktop/desktop_native/core/src/password/windows.rs +++ b/apps/desktop/desktop_native/core/src/password/windows.rs @@ -13,7 +13,7 @@ use windows::{ const CRED_FLAGS_NONE: u32 = 0; -pub fn get_password<'a>(service: &str, account: &str) -> Result { +pub async fn get_password<'a>(service: &str, account: &str) -> Result { let target_name = U16CString::from_str(target_name(service, account))?; let mut credential: *mut CREDENTIALW = std::ptr::null_mut(); @@ -45,39 +45,7 @@ pub fn get_password<'a>(service: &str, account: &str) -> Result { Ok(String::from(password)) } -// Remove this after sufficient releases -pub fn get_password_keytar<'a>(service: &str, account: &str) -> Result { - let target_name = U16CString::from_str(target_name(service, account))?; - - let mut credential: *mut CREDENTIALW = std::ptr::null_mut(); - let credential_ptr = &mut credential; - - let result = unsafe { - CredReadW( - PCWSTR(target_name.as_ptr()), - CRED_TYPE_GENERIC, - CRED_FLAGS_NONE, - credential_ptr, - ) - }; - - scopeguard::defer!({ - unsafe { CredFree(credential as *mut _) }; - }); - - result?; - - let password = unsafe { - std::str::from_utf8_unchecked(std::slice::from_raw_parts( - (*credential).CredentialBlob, - (*credential).CredentialBlobSize as usize, - )) - }; - - Ok(String::from(password)) -} - -pub fn set_password(service: &str, account: &str, password: &str) -> Result<()> { +pub async fn set_password(service: &str, account: &str, password: &str) -> Result<()> { let mut target_name = U16CString::from_str(target_name(service, account))?; let mut user_name = U16CString::from_str(account)?; let last_written = FILETIME { @@ -108,7 +76,7 @@ pub fn set_password(service: &str, account: &str, password: &str) -> Result<()> Ok(()) } -pub fn delete_password(service: &str, account: &str) -> Result<()> { +pub async fn delete_password(service: &str, account: &str) -> Result<()> { let target_name = U16CString::from_str(target_name(service, account))?; unsafe { @@ -122,7 +90,7 @@ pub fn delete_password(service: &str, account: &str) -> Result<()> { Ok(()) } -pub fn is_available() -> Result { +pub async fn is_available() -> Result { Ok(true) } @@ -142,36 +110,25 @@ fn convert_error(e: windows::core::Error) -> String { mod tests { use super::*; - #[test] - fn test() { - scopeguard::defer!(delete_password("BitwardenTest", "BitwardenTest").unwrap_or({});); - set_password("BitwardenTest", "BitwardenTest", "Random").unwrap(); + #[tokio::test] + async fn test() { + set_password("BitwardenTest", "BitwardenTest", "Random").await.unwrap(); assert_eq!( "Random", - get_password("BitwardenTest", "BitwardenTest").unwrap() + get_password("BitwardenTest", "BitwardenTest").await.unwrap() ); - delete_password("BitwardenTest", "BitwardenTest").unwrap(); + delete_password("BitwardenTest", "BitwardenTest").await.unwrap(); // Ensure password is deleted - match get_password("BitwardenTest", "BitwardenTest") { + match get_password("BitwardenTest", "BitwardenTest").await { Ok(_) => panic!("Got a result"), Err(e) => assert_eq!("Password not found.", e.to_string()), } } - #[test] - fn test_get_password_keytar() { - scopeguard::defer!(delete_password("BitwardenTest", "BitwardenTest").unwrap_or({});); - keytar::set_password("BitwardenTest", "BitwardenTest", "HelloFromKeytar").unwrap(); - assert_eq!( - "HelloFromKeytar", - get_password_keytar("BitwardenTest", "BitwardenTest").unwrap() - ); - } - - #[test] - fn test_error_no_password() { - match get_password("BitwardenTest", "BitwardenTest") { + #[tokio::test] + async fn test_error_no_password() { + match get_password("BitwardenTest", "BitwardenTest").await { Ok(_) => panic!("Got a result"), Err(e) => assert_eq!("Password not found.", e.to_string()), } diff --git a/apps/desktop/desktop_native/napi/index.d.ts b/apps/desktop/desktop_native/napi/index.d.ts index 009f29333a5..9ceb30c4ff5 100644 --- a/apps/desktop/desktop_native/napi/index.d.ts +++ b/apps/desktop/desktop_native/napi/index.d.ts @@ -6,8 +6,6 @@ export declare namespace passwords { /** Fetch the stored password from the keychain. */ export function getPassword(service: string, account: string): Promise - /** Fetch the stored password from the keychain that was stored with Keytar. */ - export function getPasswordKeytar(service: string, account: string): Promise /** Save the password to the keychain. Adds an entry if none exists otherwise updates the existing entry. */ export function setPassword(service: string, account: string, password: string): Promise /** Delete the stored password from the keychain. */ diff --git a/apps/desktop/desktop_native/napi/src/lib.rs b/apps/desktop/desktop_native/napi/src/lib.rs index c5ca655bce6..f160b19ad53 100644 --- a/apps/desktop/desktop_native/napi/src/lib.rs +++ b/apps/desktop/desktop_native/napi/src/lib.rs @@ -8,14 +8,7 @@ pub mod passwords { /// Fetch the stored password from the keychain. #[napi] pub async fn get_password(service: String, account: String) -> napi::Result { - desktop_core::password::get_password(&service, &account) - .map_err(|e| napi::Error::from_reason(e.to_string())) - } - - /// Fetch the stored password from the keychain that was stored with Keytar. - #[napi] - pub async fn get_password_keytar(service: String, account: String) -> napi::Result { - desktop_core::password::get_password_keytar(&service, &account) + desktop_core::password::get_password(&service, &account).await .map_err(|e| napi::Error::from_reason(e.to_string())) } @@ -26,21 +19,21 @@ pub mod passwords { account: String, password: String, ) -> napi::Result<()> { - desktop_core::password::set_password(&service, &account, &password) + desktop_core::password::set_password(&service, &account, &password).await .map_err(|e| napi::Error::from_reason(e.to_string())) } /// Delete the stored password from the keychain. #[napi] pub async fn delete_password(service: String, account: String) -> napi::Result<()> { - desktop_core::password::delete_password(&service, &account) + desktop_core::password::delete_password(&service, &account).await .map_err(|e| napi::Error::from_reason(e.to_string())) } // Checks if the os secure storage is available #[napi] pub async fn is_available() -> napi::Result { - desktop_core::password::is_available().map_err(|e| napi::Error::from_reason(e.to_string())) + desktop_core::password::is_available().await.map_err(|e| napi::Error::from_reason(e.to_string())) } } @@ -81,6 +74,7 @@ pub mod biometrics { key_material.map(|m| m.into()), &iv_b64, ) + .await .map_err(|e| napi::Error::from_reason(e.to_string())) } @@ -92,6 +86,7 @@ pub mod biometrics { ) -> napi::Result { let result = Biometric::get_biometric_secret(&service, &account, key_material.map(|m| m.into())) + .await .map_err(|e| napi::Error::from_reason(e.to_string())); result } diff --git a/apps/desktop/electron-builder.json b/apps/desktop/electron-builder.json index e4649a083a3..9b894b0bfc7 100644 --- a/apps/desktop/electron-builder.json +++ b/apps/desktop/electron-builder.json @@ -224,7 +224,7 @@ }, "deb": { "artifactName": "${productName}-${version}-${arch}.${ext}", - "depends": ["libnotify4", "libxtst6", "libnss3", "libsecret-1-0", "libxss1"] + "depends": ["libnotify4", "libxtst6", "libnss3", "libxss1"] }, "appImage": { "artifactName": "${productName}-${version}-${arch}.${ext}" From 811b97cef5cef45fea777f0d74a3bc392958683f Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Wed, 4 Dec 2024 17:04:08 +0100 Subject: [PATCH 23/46] [PM-15541] Hide ssh key filter behind feature flag (#12239) * Hide ssh key filter behind feature flag * Hide ssh keys in web client by featureflag * Fix build --- .../components/vault/vault-filter.component.html | 1 + .../components/vault/vault-filter.component.ts | 7 +++++++ .../filters/type-filter.component.html | 1 + .../filters/type-filter.component.ts | 16 ++++++++++++++-- .../components/vault-filter.component.ts | 12 +++++++++--- .../vault-filter/vault-filter.component.ts | 3 +++ 6 files changed, 35 insertions(+), 5 deletions(-) diff --git a/apps/browser/src/vault/popup/components/vault/vault-filter.component.html b/apps/browser/src/vault/popup/components/vault/vault-filter.component.html index f5c28b2bebd..bf557f74608 100644 --- a/apps/browser/src/vault/popup/components/vault/vault-filter.component.html +++ b/apps/browser/src/vault/popup/components/vault/vault-filter.component.html @@ -118,6 +118,7 @@ type="button" class="box-content-row" appStopClick + *ngIf="isSshKeysEnabled" (click)="selectType(cipherType.SshKey)" >
diff --git a/apps/browser/src/vault/popup/components/vault/vault-filter.component.ts b/apps/browser/src/vault/popup/components/vault/vault-filter.component.ts index 448f85a8cbd..d12b2fd801d 100644 --- a/apps/browser/src/vault/popup/components/vault/vault-filter.component.ts +++ b/apps/browser/src/vault/popup/components/vault/vault-filter.component.ts @@ -7,7 +7,9 @@ import { first, switchMap, takeUntil } from "rxjs/operators"; import { CollectionView } from "@bitwarden/admin-console/common"; import { VaultFilter } from "@bitwarden/angular/vault/vault-filter/models/vault-filter.model"; import { SearchService } from "@bitwarden/common/abstractions/search.service"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { SyncService } from "@bitwarden/common/platform/sync"; @@ -62,6 +64,8 @@ export class VaultFilterComponent implements OnInit, OnDestroy { selectedOrganization: string = null; showCollections = true; + isSshKeysEnabled = false; + private loadedTimeout: number; private selectedTimeout: number; private preventSelected = false; @@ -95,6 +99,7 @@ export class VaultFilterComponent implements OnInit, OnDestroy { private location: Location, private vaultFilterService: VaultFilterService, private vaultBrowserStateService: VaultBrowserStateService, + private configService: ConfigService, ) { this.noFolderListSize = 100; } @@ -166,6 +171,8 @@ export class VaultFilterComponent implements OnInit, OnDestroy { .subscribe((isSearchable) => { this.isSearchable = isSearchable; }); + + this.isSshKeysEnabled = await this.configService.getFeatureFlag(FeatureFlag.SSHKeyVaultItem); } ngOnDestroy() { diff --git a/apps/desktop/src/vault/app/vault/vault-filter/filters/type-filter.component.html b/apps/desktop/src/vault/app/vault/vault-filter/filters/type-filter.component.html index c3dcd191dfc..55e10980ad1 100644 --- a/apps/desktop/src/vault/app/vault/vault-filter/filters/type-filter.component.html +++ b/apps/desktop/src/vault/app/vault/vault-filter/filters/type-filter.component.html @@ -82,6 +82,7 @@
  • - diff --git a/apps/browser/src/vault/popup/settings/trash-list-items-container/trash-list-items-container.component.ts b/apps/browser/src/vault/popup/settings/trash-list-items-container/trash-list-items-container.component.ts index de9d95aab00..6827823704e 100644 --- a/apps/browser/src/vault/popup/settings/trash-list-items-container/trash-list-items-container.component.ts +++ b/apps/browser/src/vault/popup/settings/trash-list-items-container/trash-list-items-container.component.ts @@ -1,5 +1,5 @@ import { CommonModule } from "@angular/common"; -import { Component, Input } from "@angular/core"; +import { ChangeDetectionStrategy, Component, Input } from "@angular/core"; import { Router } from "@angular/router"; import { JslibModule } from "@bitwarden/angular/jslib.module"; @@ -17,7 +17,7 @@ import { ToastService, TypographyModule, } from "@bitwarden/components"; -import { PasswordRepromptService } from "@bitwarden/vault"; +import { CanDeleteCipherDirective, PasswordRepromptService } from "@bitwarden/vault"; @Component({ selector: "app-trash-list-items-container", @@ -29,10 +29,12 @@ import { PasswordRepromptService } from "@bitwarden/vault"; JslibModule, SectionComponent, SectionHeaderComponent, + CanDeleteCipherDirective, MenuModule, IconButtonModule, TypographyModule, ], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class TrashListItemsContainerComponent { /** diff --git a/apps/browser/src/vault/popup/settings/trash.component.ts b/apps/browser/src/vault/popup/settings/trash.component.ts index b6f77ef6a52..8bac22df53f 100644 --- a/apps/browser/src/vault/popup/settings/trash.component.ts +++ b/apps/browser/src/vault/popup/settings/trash.component.ts @@ -1,5 +1,5 @@ import { CommonModule } from "@angular/common"; -import { Component } from "@angular/core"; +import { ChangeDetectionStrategy, Component } from "@angular/core"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { CalloutModule, NoItemsModule } from "@bitwarden/components"; @@ -27,6 +27,7 @@ import { TrashListItemsContainerComponent } from "./trash-list-items-container/t CalloutModule, NoItemsModule, ], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class TrashComponent { protected deletedCiphers$ = this.vaultPopupItemsService.deletedCiphers$; diff --git a/libs/vault/src/components/can-delete-cipher.directive.ts b/libs/vault/src/components/can-delete-cipher.directive.ts new file mode 100644 index 00000000000..c1c7706a1fa --- /dev/null +++ b/libs/vault/src/components/can-delete-cipher.directive.ts @@ -0,0 +1,42 @@ +import { Directive, Input, OnDestroy, TemplateRef, ViewContainerRef } from "@angular/core"; +import { Subject, takeUntil } from "rxjs"; + +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service"; + +/** + * Only shows the element if the user can delete the cipher. + */ +@Directive({ + selector: "[appCanDeleteCipher]", + standalone: true, +}) +export class CanDeleteCipherDirective implements OnDestroy { + private destroy$ = new Subject(); + + @Input("appCanDeleteCipher") set cipher(cipher: CipherView) { + this.viewContainer.clear(); + + this.cipherAuthorizationService + .canDeleteCipher$(cipher) + .pipe(takeUntil(this.destroy$)) + .subscribe((canDelete: boolean) => { + if (canDelete) { + this.viewContainer.createEmbeddedView(this.templateRef); + } else { + this.viewContainer.clear(); + } + }); + } + + constructor( + private templateRef: TemplateRef, + private viewContainer: ViewContainerRef, + private cipherAuthorizationService: CipherAuthorizationService, + ) {} + + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + } +} diff --git a/libs/vault/src/index.ts b/libs/vault/src/index.ts index f6a95281f81..dca9b2dee79 100644 --- a/libs/vault/src/index.ts +++ b/libs/vault/src/index.ts @@ -2,6 +2,7 @@ export { PasswordRepromptService } from "./services/password-reprompt.service"; export { CopyCipherFieldService, CopyAction } from "./services/copy-cipher-field.service"; export { CopyCipherFieldDirective } from "./components/copy-cipher-field.directive"; export { OrgIconDirective } from "./components/org-icon.directive"; +export { CanDeleteCipherDirective } from "./components/can-delete-cipher.directive"; export * from "./cipher-view"; export * from "./cipher-form"; From 0bc63517bdda7187fd57a5fbcd0d31f5dfbb7a44 Mon Sep 17 00:00:00 2001 From: Conner Turnbull <133619638+cturnbull-bitwarden@users.noreply.github.com> Date: Wed, 4 Dec 2024 12:25:15 -0500 Subject: [PATCH 26/46] If user can't view subscription, don't show upgrade modal (#12248) --- .../admin-console/organizations/members/members.component.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apps/web/src/app/admin-console/organizations/members/members.component.ts b/apps/web/src/app/admin-console/organizations/members/members.component.ts index b33807c66c9..26e27e1249b 100644 --- a/apps/web/src/app/admin-console/organizations/members/members.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/members.component.ts @@ -487,6 +487,11 @@ export class MembersComponent extends BaseMembersComponent this.organization.productTierType === ProductTierType.TeamsStarter || this.organization.productTierType === ProductTierType.Families) ) { + if (!this.organization.canEditSubscription) { + await this.showSeatLimitReachedDialog(); + return; + } + const reference = openChangePlanDialog(this.dialogService, { data: { organizationId: this.organization.id, From cee13556af99bb98451fd87a67c6edb5d449eef6 Mon Sep 17 00:00:00 2001 From: rr-bw <102181210+rr-bw@users.noreply.github.com> Date: Wed, 4 Dec 2024 14:56:59 -0800 Subject: [PATCH 27/46] fix(ui): [PM-13147] Change logout `a` link to `button` so that it displays correctly (#12232) On `/update-temp-password`, on the light theme the "Logout" link was not showing in the upper-left corner because the text color for an `a` link is our primary blue color, which blended in with the headers background color. This PR changes the logout `a` link to a `button` so that it inherits the header text color. It should be a button anyway since it's calling a function and not simply routing. --- apps/browser/src/auth/popup/update-temp-password.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/browser/src/auth/popup/update-temp-password.component.html b/apps/browser/src/auth/popup/update-temp-password.component.html index 6e0cc0f4483..0ce82aa20cf 100644 --- a/apps/browser/src/auth/popup/update-temp-password.component.html +++ b/apps/browser/src/auth/popup/update-temp-password.component.html @@ -1,7 +1,7 @@
    - {{ "logOut" | i18n }} +

    {{ "updateMasterPassword" | i18n }} From 773aba4fef5fb599a3a89e718e82af68e4bf23ff Mon Sep 17 00:00:00 2001 From: Shane Melton Date: Wed, 4 Dec 2024 15:30:15 -0800 Subject: [PATCH 28/46] [PM-15559] Fix hide passwords in AC for users that have view, except password (#12252) * [PM-15559] Update admin dialog flows to first try the local state before using the API when not required * [PM-15559] Clear initial values after creating a new cipher so that they do not override the newly created cipher for subsequent edits --- .../vault-item-dialog.component.ts | 12 +++--- ...console-cipher-form-config.service.spec.ts | 43 +++++++++++++------ ...dmin-console-cipher-form-config.service.ts | 26 ++++++++--- 3 files changed, 57 insertions(+), 24 deletions(-) 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 2742cd52ef1..2f5b014fcb3 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 @@ -281,17 +281,19 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy { // If the cipher was newly created (via add/clone), switch the form to edit for subsequent edits. if (this._originalFormMode === "add" || this._originalFormMode === "clone") { this.formConfig.mode = "edit"; + this.formConfig.initialValues = null; } - let cipher: Cipher; + let cipher = await this.cipherService.get(cipherView.id); - // When the form config is used within the Admin Console, retrieve the cipher from the admin endpoint - if (this.formConfig.isAdminConsole) { + // When the form config is used within the Admin Console, retrieve the cipher from the admin endpoint (if not found in local state) + if (this.formConfig.isAdminConsole && (cipher == null || this.formConfig.admin)) { const cipherResponse = await this.apiService.getCipherAdmin(cipherView.id); + cipherResponse.edit = true; + cipherResponse.viewPassword = true; + const cipherData = new CipherData(cipherResponse); cipher = new Cipher(cipherData); - } else { - cipher = await this.cipherService.get(cipherView.id); } // Store the updated cipher so any following edits use the most up to date cipher diff --git a/apps/web/src/app/vault/org-vault/services/admin-console-cipher-form-config.service.spec.ts b/apps/web/src/app/vault/org-vault/services/admin-console-cipher-form-config.service.spec.ts index 05c40fe2e79..25976c4fb82 100644 --- a/apps/web/src/app/vault/org-vault/services/admin-console-cipher-form-config.service.spec.ts +++ b/apps/web/src/app/vault/org-vault/services/admin-console-cipher-form-config.service.spec.ts @@ -8,6 +8,7 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli import { OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { CipherId } from "@bitwarden/common/types/guid"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { RoutedVaultFilterService } from "../../individual-vault/vault-filter/services/routed-vault-filter.service"; @@ -52,12 +53,16 @@ describe("AdminConsoleCipherFormConfigService", () => { const organization$ = new BehaviorSubject(testOrg as Organization); const organizations$ = new BehaviorSubject([testOrg, testOrg2] as Organization[]); const getCipherAdmin = jest.fn().mockResolvedValue(null); + const getCipher = jest.fn().mockResolvedValue(null); beforeEach(async () => { getCipherAdmin.mockClear(); getCipherAdmin.mockResolvedValue({ id: cipherId, name: "Test Cipher - (admin)" }); - await TestBed.configureTestingModule({ + getCipher.mockClear(); + getCipher.mockResolvedValue({ id: cipherId, name: "Test Cipher" }); + + TestBed.configureTestingModule({ providers: [ AdminConsoleCipherFormConfigService, { provide: OrganizationService, useValue: { get$: () => organization$, organizations$ } }, @@ -74,14 +79,14 @@ describe("AdminConsoleCipherFormConfigService", () => { useValue: { filter$: new BehaviorSubject({ organizationId: testOrg.id }) }, }, { provide: ApiService, useValue: { getCipherAdmin } }, + { provide: CipherService, useValue: { get: getCipher } }, ], }); + adminConsoleConfigService = TestBed.inject(AdminConsoleCipherFormConfigService); }); describe("buildConfig", () => { it("sets individual attributes", async () => { - adminConsoleConfigService = TestBed.inject(AdminConsoleCipherFormConfigService); - const { folders, hideIndividualVaultFields } = await adminConsoleConfigService.buildConfig( "add", cipherId, @@ -92,8 +97,6 @@ describe("AdminConsoleCipherFormConfigService", () => { }); it("sets mode based on passed mode", async () => { - adminConsoleConfigService = TestBed.inject(AdminConsoleCipherFormConfigService); - const { mode } = await adminConsoleConfigService.buildConfig("edit", cipherId); expect(mode).toBe("edit"); @@ -122,8 +125,6 @@ describe("AdminConsoleCipherFormConfigService", () => { }); it("sets `allowPersonalOwnership`", async () => { - adminConsoleConfigService = TestBed.inject(AdminConsoleCipherFormConfigService); - policyAppliesToActiveUser$.next(true); let result = await adminConsoleConfigService.buildConfig("clone", cipherId); @@ -138,8 +139,6 @@ describe("AdminConsoleCipherFormConfigService", () => { }); it("disables personal ownership when not cloning", async () => { - adminConsoleConfigService = TestBed.inject(AdminConsoleCipherFormConfigService); - policyAppliesToActiveUser$.next(false); let result = await adminConsoleConfigService.buildConfig("add", cipherId); @@ -172,14 +171,32 @@ describe("AdminConsoleCipherFormConfigService", () => { expect(result.organizations).toEqual([testOrg, testOrg2]); }); - it("retrieves the cipher from the admin service", async () => { + it("retrieves the cipher from the admin service when canEditAllCiphers is true", async () => { getCipherAdmin.mockResolvedValue({ id: cipherId, name: "Test Cipher - (admin)" }); + testOrg.canEditAllCiphers = true; - adminConsoleConfigService = TestBed.inject(AdminConsoleCipherFormConfigService); - - await adminConsoleConfigService.buildConfig("add", cipherId); + await adminConsoleConfigService.buildConfig("edit", cipherId); expect(getCipherAdmin).toHaveBeenCalledWith(cipherId); }); + + it("retrieves the cipher from the admin service when not found in local state", async () => { + getCipherAdmin.mockResolvedValue({ id: cipherId, name: "Test Cipher - (admin)" }); + testOrg.canEditAllCiphers = false; + getCipher.mockResolvedValue(null); + + await adminConsoleConfigService.buildConfig("edit", cipherId); + + expect(getCipherAdmin).toHaveBeenCalledWith(cipherId); + }); + + it("retrieves the cipher from local state when admin is not required", async () => { + testOrg.canEditAllCiphers = false; + + await adminConsoleConfigService.buildConfig("edit", cipherId); + + expect(getCipherAdmin).not.toHaveBeenCalled(); + expect(getCipher).toHaveBeenCalledWith(cipherId); + }); }); }); 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 457b4e83d03..50439a4d8dc 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 @@ -5,8 +5,10 @@ import { CollectionAdminService } from "@bitwarden/admin-console/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; -import { PolicyType, OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums"; +import { OrganizationUserStatusType, PolicyType } from "@bitwarden/common/admin-console/enums"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { CipherId } from "@bitwarden/common/types/guid"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherData } from "@bitwarden/common/vault/models/data/cipher.data"; import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; @@ -25,6 +27,7 @@ export class AdminConsoleCipherFormConfigService implements CipherFormConfigServ private organizationService: OrganizationService = inject(OrganizationService); private routedVaultFilterService: RoutedVaultFilterService = inject(RoutedVaultFilterService); private collectionAdminService: CollectionAdminService = inject(CollectionAdminService); + private cipherService: CipherService = inject(CipherService); private apiService: ApiService = inject(ApiService); private allowPersonalOwnership$ = this.policyService @@ -57,7 +60,6 @@ export class AdminConsoleCipherFormConfigService implements CipherFormConfigServ cipherId?: CipherId, cipherType?: CipherType, ): Promise { - const cipher = await this.getCipher(cipherId); const [organization, allowPersonalOwnership, allOrganizations, allCollections] = await firstValueFrom( combineLatest([ @@ -74,7 +76,7 @@ export class AdminConsoleCipherFormConfigService implements CipherFormConfigServ // Only allow the user to assign to their personal vault when cloning and // the policies are enabled for it. const allowPersonalOwnershipOnlyForClone = mode === "clone" ? allowPersonalOwnership : false; - + const cipher = await this.getCipher(cipherId, organization); return { mode, cipherType: cipher?.type ?? cipherType ?? CipherType.Login, @@ -89,14 +91,26 @@ export class AdminConsoleCipherFormConfigService implements CipherFormConfigServ }; } - private async getCipher(id?: CipherId): Promise { + private async getCipher(id: CipherId | null, organization: Organization): Promise { if (id == null) { - return Promise.resolve(null); + return null; } - // Retrieve the cipher through the means of an admin + const localCipher = await this.cipherService.get(id); + + // Fetch from the API because we don't need the permissions in local state OR the cipher was not found (e.g. unassigned) + if (organization.canEditAllCiphers || localCipher == null) { + return await this.getCipherFromAdminApi(id); + } + + return localCipher; + } + + private async getCipherFromAdminApi(id: CipherId): Promise { const cipherResponse = await this.apiService.getCipherAdmin(id); + // Ensure admin response includes permissions that allow editing cipherResponse.edit = true; + cipherResponse.viewPassword = true; const cipherData = new CipherData(cipherResponse); return new Cipher(cipherData); From 816bf70dc0f1160ff1846a4b2f04b15289527b42 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Thu, 5 Dec 2024 11:53:30 +0100 Subject: [PATCH 29/46] Disable ssh agent for organization items (#12240) --- apps/desktop/src/platform/services/ssh-agent.service.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/platform/services/ssh-agent.service.ts b/apps/desktop/src/platform/services/ssh-agent.service.ts index 969300fcc5f..a3e6fb09040 100644 --- a/apps/desktop/src/platform/services/ssh-agent.service.ts +++ b/apps/desktop/src/platform/services/ssh-agent.service.ts @@ -198,7 +198,10 @@ export class SshAgentService implements OnDestroy { } const sshCiphers = ciphers.filter( - (cipher) => cipher.type === CipherType.SshKey && !cipher.isDeleted, + (cipher) => + cipher.type === CipherType.SshKey && + !cipher.isDeleted && + cipher.organizationId === null, ); const keys = sshCiphers.map((cipher) => { return { From f8ba01d3fa2515d29deb7a104a21f9521b085e82 Mon Sep 17 00:00:00 2001 From: Robyn MacCallum Date: Thu, 5 Dec 2024 09:46:56 -0500 Subject: [PATCH 30/46] Remove SM team from CODEOWNERS (#12258) --- .github/CODEOWNERS | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index cf656967da0..12a8c182335 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -4,10 +4,6 @@ # # https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners -## Secrets Manager team files ## -bitwarden_license/bit-web/src/app/secrets-manager @bitwarden/team-secrets-manager-dev -apps/web/src/app/secrets-manager/ @bitwarden/team-secrets-manager-dev - ## Auth team files ## apps/browser/src/auth @bitwarden/team-auth-dev apps/cli/src/auth @bitwarden/team-auth-dev From 6dc68b174b07a9d82bb862350353ab6fb0506b0f Mon Sep 17 00:00:00 2001 From: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> Date: Thu, 5 Dec 2024 15:48:03 +0100 Subject: [PATCH 31/46] Prompt user to popout the extension when creating a file send with Chrome on MacOS (#12257) Co-authored-by: Daniel James Smith --- .../src/tools/popup/services/file-popout-utils.service.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/apps/browser/src/tools/popup/services/file-popout-utils.service.ts b/apps/browser/src/tools/popup/services/file-popout-utils.service.ts index 65a311e47c3..fa72e411316 100644 --- a/apps/browser/src/tools/popup/services/file-popout-utils.service.ts +++ b/apps/browser/src/tools/popup/services/file-popout-utils.service.ts @@ -49,7 +49,7 @@ export class FilePopoutUtilsService { } /** - * Determines whether to show a file popout callout message for Chromium-based browsers in Linux and Mac OS X Big Sur + * Determines whether to show a file popout callout message for Chromium-based browsers in Linux and Mac OS X * @param win - The window context in which the check should be performed. * @returns True if the extension is not in a sidebar or popout; otherwise, false. */ @@ -66,8 +66,6 @@ export class FilePopoutUtilsService { } private isUnsupportedMac(win: Window): boolean { - return ( - this.platformUtilsService.isChrome() && win?.navigator?.appVersion.includes("Mac OS X 11") - ); + return this.platformUtilsService.isChrome() && win?.navigator?.appVersion.includes("Mac OS X"); } } From c11f429ddb6a48fb6018777041decc35b09ad32c Mon Sep 17 00:00:00 2001 From: Brandon Treston Date: Thu, 5 Dec 2024 10:09:40 -0500 Subject: [PATCH 32/46] [PM-12273] Admin Console Integration Page (#11883) * Integration page initial implementation * replace placeholder integrations * fix linting and tests * fix locales * update locale * Change logos, add link to SCIM page * refactor to standalone components, add integration filtering pipe * refactor modules and imports. Remove hyperlink text from integration card template * refactor i18n usage to be more generic * Add storybooks * fix tests * minify svgs, include spec files in TS config, fix stories * Update apps/web/src/locales/en/messages.json Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com> * fix imports, add static dir for stories * Add darkmode svgs for integrations * hide nav link for non enterprise orgs * add router guard * change rxjs selector * Remove tailwind class causing style issues --------- Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com> --- .storybook/main.ts | 1 + .../guards/is-enterprise-org.guard.spec.ts | 26 ++- .../guards/is-enterprise-org.guard.ts | 4 +- .../integrations/integrations.component.html | 66 ++++++ .../integrations/integrations.component.ts | 207 ++++++++++++++++++ .../organization-layout.component.html | 6 + .../layouts/organization-layout.component.ts | 14 ++ .../organization-routing.module.ts | 15 ++ apps/web/src/app/shared/components/index.ts | 4 + .../integration-card.component.html | 10 +- .../integration-card.component.spec.ts | 23 +- .../integration-card.component.ts | 7 +- .../integration-card.stories.ts | 63 ++++++ .../integration-grid.component.html | 11 +- .../integration-grid.component.spec.ts | 39 +++- .../integration-grid.component.ts | 22 ++ .../integration-grid.stories.ts | 77 +++++++ .../integrations/integrations.pipe.ts | 15 ++ .../shared/components/integrations/models.ts | 1 - apps/web/src/app/shared/index.ts | 1 + .../web/src/images/integrations/aws-color.svg | 1 + .../src/images/integrations/aws-darkmode.svg | 1 + .../integrations/azure-active-directory.svg | 1 + .../integrations/bitwarden-vertical-blue.svg | 1 + .../integrations/jumpcloud-darkmode.svg | 1 + .../integrations/logo-auth0-badge-color.svg | 1 + .../images/integrations/logo-duo-color.svg | 1 + .../integrations/logo-elastic-badge-color.svg | 1 + .../integrations/logo-google-badge-color.svg | 1 + .../logo-jumpcloud-badge-color.svg | 1 + .../integrations/logo-keycloak-icon.svg | 23 ++ .../logo-microsoft-entra-id-color.svg | 1 + .../logo-microsoft-intune-color.svg | 1 + .../logo-microsoft-sentinel-color.svg | 1 + .../integrations/logo-okta-symbol-black.svg | 1 + .../logo-onelogin-badge-color.svg | 1 + .../integrations/logo-panther-round-color.svg | 1 + .../logo-ping-identity-badge-color.svg | 1 + .../images/integrations/logo-rapid7-black.svg | 1 + .../images/integrations/logo-splunk-black.svg | 1 + .../src/images/integrations/okta-darkmode.svg | 1 + .../images/integrations/onelogin-darkmode.svg | 1 + .../images/integrations/rapid7-darkmode.svg | 1 + .../images/integrations/splunk-darkmode.svg | 1 + apps/web/src/locales/en/messages.json | 115 +++++++--- .../organizations/manage/scim.component.html | 7 +- .../integration-grid.component.ts | 15 -- .../integrations/integrations.component.html | 12 +- .../integrations.component.spec.ts | 21 +- .../integrations/integrations.component.ts | 19 +- .../integrations/integrations.module.ts | 17 +- bitwarden_license/bit-web/tsconfig.json | 3 +- libs/common/src/enums/feature-flag.enum.ts | 2 + .../common/src/enums/integration-type.enum.ts | 5 + 54 files changed, 764 insertions(+), 110 deletions(-) create mode 100644 apps/web/src/app/admin-console/organizations/integrations/integrations.component.html create mode 100644 apps/web/src/app/admin-console/organizations/integrations/integrations.component.ts create mode 100644 apps/web/src/app/shared/components/index.ts rename {bitwarden_license/bit-web/src/app/secrets-manager => apps/web/src/app/shared/components}/integrations/integration-card/integration-card.component.html (64%) rename {bitwarden_license/bit-web/src/app/secrets-manager => apps/web/src/app/shared/components}/integrations/integration-card/integration-card.component.spec.ts (86%) rename {bitwarden_license/bit-web/src/app/secrets-manager => apps/web/src/app/shared/components}/integrations/integration-card/integration-card.component.ts (95%) create mode 100644 apps/web/src/app/shared/components/integrations/integration-card/integration-card.stories.ts rename {bitwarden_license/bit-web/src/app/secrets-manager => apps/web/src/app/shared/components}/integrations/integration-grid/integration-grid.component.html (66%) rename {bitwarden_license/bit-web/src/app/secrets-manager => apps/web/src/app/shared/components}/integrations/integration-grid/integration-grid.component.spec.ts (61%) create mode 100644 apps/web/src/app/shared/components/integrations/integration-grid/integration-grid.component.ts create mode 100644 apps/web/src/app/shared/components/integrations/integration-grid/integration-grid.stories.ts create mode 100644 apps/web/src/app/shared/components/integrations/integrations.pipe.ts rename bitwarden_license/bit-web/src/app/secrets-manager/integrations/models/integration.ts => apps/web/src/app/shared/components/integrations/models.ts (95%) create mode 100644 apps/web/src/images/integrations/aws-color.svg create mode 100644 apps/web/src/images/integrations/aws-darkmode.svg create mode 100644 apps/web/src/images/integrations/azure-active-directory.svg create mode 100644 apps/web/src/images/integrations/bitwarden-vertical-blue.svg create mode 100644 apps/web/src/images/integrations/jumpcloud-darkmode.svg create mode 100644 apps/web/src/images/integrations/logo-auth0-badge-color.svg create mode 100644 apps/web/src/images/integrations/logo-duo-color.svg create mode 100644 apps/web/src/images/integrations/logo-elastic-badge-color.svg create mode 100644 apps/web/src/images/integrations/logo-google-badge-color.svg create mode 100644 apps/web/src/images/integrations/logo-jumpcloud-badge-color.svg create mode 100644 apps/web/src/images/integrations/logo-keycloak-icon.svg create mode 100644 apps/web/src/images/integrations/logo-microsoft-entra-id-color.svg create mode 100644 apps/web/src/images/integrations/logo-microsoft-intune-color.svg create mode 100644 apps/web/src/images/integrations/logo-microsoft-sentinel-color.svg create mode 100644 apps/web/src/images/integrations/logo-okta-symbol-black.svg create mode 100644 apps/web/src/images/integrations/logo-onelogin-badge-color.svg create mode 100644 apps/web/src/images/integrations/logo-panther-round-color.svg create mode 100644 apps/web/src/images/integrations/logo-ping-identity-badge-color.svg create mode 100644 apps/web/src/images/integrations/logo-rapid7-black.svg create mode 100644 apps/web/src/images/integrations/logo-splunk-black.svg create mode 100644 apps/web/src/images/integrations/okta-darkmode.svg create mode 100644 apps/web/src/images/integrations/onelogin-darkmode.svg create mode 100644 apps/web/src/images/integrations/rapid7-darkmode.svg create mode 100644 apps/web/src/images/integrations/splunk-darkmode.svg delete mode 100644 bitwarden_license/bit-web/src/app/secrets-manager/integrations/integration-grid/integration-grid.component.ts diff --git a/.storybook/main.ts b/.storybook/main.ts index 454da4377dc..b48a86ba2b2 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -57,6 +57,7 @@ const config: StorybookConfig = { return config; }, docs: {}, + staticDirs: ["../apps/web/src/images"], }; export default config; diff --git a/apps/web/src/app/admin-console/organizations/guards/is-enterprise-org.guard.spec.ts b/apps/web/src/app/admin-console/organizations/guards/is-enterprise-org.guard.spec.ts index 75e63d42428..5d138e8137d 100644 --- a/apps/web/src/app/admin-console/organizations/guards/is-enterprise-org.guard.spec.ts +++ b/apps/web/src/app/admin-console/organizations/guards/is-enterprise-org.guard.spec.ts @@ -59,8 +59,14 @@ describe("Is Enterprise Org Guard", () => { { path: "organizations/:organizationId/enterpriseOrgsOnly", component: IsEnterpriseOrganizationComponent, - canActivate: [isEnterpriseOrgGuard()], + canActivate: [isEnterpriseOrgGuard(true)], }, + { + path: "organizations/:organizationId/enterpriseOrgsOnlyNoError", + component: IsEnterpriseOrganizationComponent, + canActivate: [isEnterpriseOrgGuard(false)], + }, + { path: "organizations/:organizationId/billing/subscription", component: OrganizationUpgradeScreenComponent, @@ -115,6 +121,24 @@ describe("Is Enterprise Org Guard", () => { ); }); + it.each([ + ProductTierType.Free, + ProductTierType.Families, + ProductTierType.Teams, + ProductTierType.TeamsStarter, + ])("does not proceed with the navigation for productTierType '%s'", async (productTierType) => { + const org = orgFactory({ + type: OrganizationUserType.User, + productTierType: productTierType, + }); + organizationService.get.calledWith(org.id).mockResolvedValue(org); + await routerHarness.navigateByUrl(`organizations/${org.id}/enterpriseOrgsOnlyNoError`); + expect(dialogService.openSimpleDialog).not.toHaveBeenCalled(); + expect( + routerHarness.routeNativeElement?.querySelector("h1")?.textContent?.trim() ?? "", + ).not.toBe("This component can only be accessed by a enterprise organization!"); + }); + it("proceeds with navigation if the organization in question is a enterprise organization", async () => { const org = orgFactory({ productTierType: ProductTierType.Enterprise }); organizationService.get.calledWith(org.id).mockResolvedValue(org); diff --git a/apps/web/src/app/admin-console/organizations/guards/is-enterprise-org.guard.ts b/apps/web/src/app/admin-console/organizations/guards/is-enterprise-org.guard.ts index 8a0d374997d..5eab89ae68a 100644 --- a/apps/web/src/app/admin-console/organizations/guards/is-enterprise-org.guard.ts +++ b/apps/web/src/app/admin-console/organizations/guards/is-enterprise-org.guard.ts @@ -17,7 +17,7 @@ import { DialogService } from "@bitwarden/components"; * if they have access to upgrade the organization. If the organization is * enterprise routing proceeds." */ -export function isEnterpriseOrgGuard(): CanActivateFn { +export function isEnterpriseOrgGuard(showError: boolean = true): CanActivateFn { return async (route: ActivatedRouteSnapshot, _state: RouterStateSnapshot) => { const router = inject(Router); const organizationService = inject(OrganizationService); @@ -29,7 +29,7 @@ export function isEnterpriseOrgGuard(): CanActivateFn { return router.createUrlTree(["/"]); } - if (org.productTierType != ProductTierType.Enterprise) { + if (org.productTierType != ProductTierType.Enterprise && showError) { // Users without billing permission can't access billing if (!org.canEditSubscription) { await dialogService.openSimpleDialog({ diff --git a/apps/web/src/app/admin-console/organizations/integrations/integrations.component.html b/apps/web/src/app/admin-console/organizations/integrations/integrations.component.html new file mode 100644 index 00000000000..61e7996becd --- /dev/null +++ b/apps/web/src/app/admin-console/organizations/integrations/integrations.component.html @@ -0,0 +1,66 @@ + + + + +
    +

    {{ "singleSignOn" | i18n }}

    +

    + {{ "ssoDescStart" | i18n }} + {{ "singleSignOn" | i18n }} + {{ "ssoDescEnd" | i18n }} +

    + +
    +
    + + +
    +

    + {{ "scimIntegration" | i18n }} +

    +

    + {{ "scimIntegrationDescStart" | i18n }} + {{ "scimIntegration" | i18n }} + {{ "scimIntegrationDescEnd" | i18n }} +

    + +
    +
    +

    + {{ "bwdc" | i18n }} +

    +

    {{ "bwdcDesc" | i18n }}

    + +
    +
    + + +
    +

    + {{ "eventManagement" | i18n }} +

    +

    {{ "eventManagementDesc" | i18n }}

    + +
    +
    + + +
    +

    + {{ "deviceManagement" | i18n }} +

    +

    {{ "deviceManagementDesc" | i18n }}

    + +
    +
    +
    diff --git a/apps/web/src/app/admin-console/organizations/integrations/integrations.component.ts b/apps/web/src/app/admin-console/organizations/integrations/integrations.component.ts new file mode 100644 index 00000000000..4b8822da7ca --- /dev/null +++ b/apps/web/src/app/admin-console/organizations/integrations/integrations.component.ts @@ -0,0 +1,207 @@ +import { Component } from "@angular/core"; + +import { IntegrationType } from "@bitwarden/common/enums"; + +import { HeaderModule } from "../../../layouts/header/header.module"; +import { FilterIntegrationsPipe, IntegrationGridComponent, Integration } from "../../../shared/"; +import { SharedModule } from "../../../shared/shared.module"; +import { SharedOrganizationModule } from "../shared"; + +@Component({ + selector: "ac-integrations", + templateUrl: "./integrations.component.html", + standalone: true, + imports: [ + SharedModule, + SharedOrganizationModule, + IntegrationGridComponent, + HeaderModule, + FilterIntegrationsPipe, + ], +}) +export class AdminConsoleIntegrationsComponent { + integrationsList: Integration[] = []; + tabIndex: number; + + constructor() { + this.integrationsList = [ + { + name: "AD FS", + linkURL: "https://bitwarden.com/help/saml-adfs/", + image: "../../../../../../../images/integrations/azure-active-directory.svg", + type: IntegrationType.SSO, + }, + { + name: "Auth0", + linkURL: "https://bitwarden.com/help/saml-auth0/", + image: "../../../../../../../images/integrations/logo-auth0-badge-color.svg", + type: IntegrationType.SSO, + }, + { + name: "AWS", + linkURL: "https://bitwarden.com/help/saml-aws/", + image: "../../../../../../../images/integrations/aws-color.svg", + imageDarkMode: "../../../../../../../images/integrations/aws-darkmode.svg", + type: IntegrationType.SSO, + }, + { + name: "Microsoft Entra ID", + linkURL: "https://bitwarden.com/help/saml-azure/", + image: "../../../../../../../images/integrations/logo-microsoft-entra-id-color.svg", + type: IntegrationType.SSO, + }, + { + name: "Duo", + linkURL: "https://bitwarden.com/help/saml-duo/", + image: "../../../../../../../images/integrations/logo-duo-color.svg", + type: IntegrationType.SSO, + }, + { + name: "Google", + linkURL: "https://bitwarden.com/help/saml-google/", + image: "../../../../../../../images/integrations/logo-google-badge-color.svg", + type: IntegrationType.SSO, + }, + { + name: "JumpCloud", + linkURL: "https://bitwarden.com/help/saml-jumpcloud/", + image: "../../../../../../../images/integrations/logo-jumpcloud-badge-color.svg", + imageDarkMode: "../../../../../../../images/integrations/jumpcloud-darkmode.svg", + type: IntegrationType.SSO, + }, + { + name: "KeyCloak", + linkURL: "https://bitwarden.com/help/saml-keycloak/", + image: "../../../../../../../images/integrations/logo-keycloak-icon.svg", + type: IntegrationType.SSO, + }, + { + name: "Okta", + linkURL: "https://bitwarden.com/help/saml-okta/", + image: "../../../../../../../images/integrations/logo-okta-symbol-black.svg", + imageDarkMode: "../../../../../../../images/integrations/okta-darkmode.svg", + type: IntegrationType.SSO, + }, + { + name: "OneLogin", + linkURL: "https://bitwarden.com/help/saml-onelogin/", + image: "../../../../../../../images/integrations/logo-onelogin-badge-color.svg", + imageDarkMode: "../../../../../../../images/integrations/onelogin-darkmode.svg", + type: IntegrationType.SSO, + }, + { + name: "PingFederate", + linkURL: "https://bitwarden.com/help/saml-pingfederate/", + image: "../../../../../../../images/integrations/logo-ping-identity-badge-color.svg", + type: IntegrationType.SSO, + }, + { + name: "Microsoft Entra ID", + linkURL: "https://bitwarden.com/help/microsoft-entra-id-scim-integration/", + image: "../../../../../../../images/integrations/logo-microsoft-entra-id-color.svg", + type: IntegrationType.SCIM, + }, + { + name: "Okta", + linkURL: "https://bitwarden.com/help/okta-scim-integration/", + image: "../../../../../../../images/integrations/logo-okta-symbol-black.svg", + imageDarkMode: "../../../../../../../images/integrations/okta-darkmode.svg", + type: IntegrationType.SCIM, + }, + { + name: "OneLogin", + linkURL: "https://bitwarden.com/help/onelogin-scim-integration/", + image: "../../../../../../../images/integrations/logo-onelogin-badge-color.svg", + imageDarkMode: "../../../../../../../images/integrations/onelogin-darkmode.svg", + type: IntegrationType.SCIM, + }, + { + name: "JumpCloud", + linkURL: "https://bitwarden.com/help/jumpcloud-scim-integration/", + image: "../../../../../../../images/integrations/logo-jumpcloud-badge-color.svg", + imageDarkMode: "../../../../../../../images/integrations/jumpcloud-darkmode.svg", + type: IntegrationType.SCIM, + }, + { + name: "Ping Identity", + linkURL: "https://bitwarden.com/help/ping-identity-scim-integration/", + image: "../../../../../../../images/integrations/logo-ping-identity-badge-color.svg", + type: IntegrationType.SCIM, + }, + { + name: "Active Directory", + linkURL: "https://bitwarden.com/help/ldap-directory/", + image: "../../../../../../../images/integrations/azure-active-directory.svg", + type: IntegrationType.BWDC, + }, + { + name: "Microsoft Entra ID", + linkURL: "https://bitwarden.com/help/microsoft-entra-id/", + image: "../../../../../../../images/integrations/logo-microsoft-entra-id-color.svg", + type: IntegrationType.BWDC, + }, + { + name: "Google Workspace", + linkURL: "https://bitwarden.com/help/workspace-directory/", + image: "../../../../../../../images/integrations/logo-google-badge-color.svg", + type: IntegrationType.BWDC, + }, + { + name: "Okta", + linkURL: "https://bitwarden.com/help/okta-directory/", + image: "../../../../../../../images/integrations/logo-okta-symbol-black.svg", + imageDarkMode: "../../../../../../../images/integrations/okta-darkmode.svg", + type: IntegrationType.BWDC, + }, + { + name: "OneLogin", + linkURL: "https://bitwarden.com/help/onelogin-directory/", + image: "../../../../../../../images/integrations/logo-onelogin-badge-color.svg", + imageDarkMode: "../../../../../../../images/integrations/onelogin-darkmode.svg", + type: IntegrationType.BWDC, + }, + { + name: "Splunk", + linkURL: "https://bitwarden.com/help/splunk-siem/", + image: "../../../../../../../images/integrations/logo-splunk-black.svg", + imageDarkMode: "../../../../../../../images/integrations/splunk-darkmode.svg", + type: IntegrationType.EVENT, + }, + { + name: "Microsoft Sentinel", + linkURL: "https://bitwarden.com/help/microsoft-sentinel-siem/", + image: "../../../../../../../images/integrations/logo-microsoft-sentinel-color.svg", + type: IntegrationType.EVENT, + }, + { + name: "Rapid7", + linkURL: "https://bitwarden.com/help/rapid7-siem/", + image: "../../../../../../../images/integrations/logo-rapid7-black.svg", + imageDarkMode: "../../../../../../../images/integrations/rapid7-darkmode.svg", + type: IntegrationType.EVENT, + }, + { + name: "Elastic", + linkURL: "https://bitwarden.com/help/elastic-siem/", + image: "../../../../../../../images/integrations/logo-elastic-badge-color.svg", + type: IntegrationType.EVENT, + }, + { + name: "Panther", + linkURL: "https://bitwarden.com/help/panther-siem/", + image: "../../../../../../../images/integrations/logo-panther-round-color.svg", + type: IntegrationType.EVENT, + }, + { + name: "Microsoft Intune", + linkURL: "https://bitwarden.com/help/deploy-browser-extensions-with-intune/", + image: "../../../../../../../images/integrations/logo-microsoft-intune-color.svg", + type: IntegrationType.DEVICE, + }, + ]; + } + + get IntegrationType(): typeof IntegrationType { + return IntegrationType; + } +} 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 1e811b6c29d..ce832ef06e9 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 @@ -60,6 +60,12 @@ + canAccessOrgAdmin(org); + protected integrationPageEnabled$: Observable; + organization$: Observable; canAccessExport$: Observable; showPaymentAndHistory$: Observable; hideNewOrgButton$: Observable; organizationIsUnmanaged$: Observable; isAccessIntelligenceFeatureEnabled = false; + enterpriseOrganization$: Observable; constructor( private route: ActivatedRoute, @@ -104,6 +108,16 @@ export class OrganizationLayoutComponent implements OnInit { provider.providerStatus !== ProviderStatusType.Billable, ), ); + + this.integrationPageEnabled$ = combineLatest( + this.organization$, + this.configService.getFeatureFlag$(FeatureFlag.PM14505AdminConsoleIntegrationPage), + ).pipe( + map( + ([org, featureFlagEnabled]) => + org.productTierType === ProductTierType.Enterprise && featureFlagEnabled, + ), + ); } canShowVaultTab(organization: Organization): boolean { diff --git a/apps/web/src/app/admin-console/organizations/organization-routing.module.ts b/apps/web/src/app/admin-console/organizations/organization-routing.module.ts index 538cc45ac63..31544968dd4 100644 --- a/apps/web/src/app/admin-console/organizations/organization-routing.module.ts +++ b/apps/web/src/app/admin-console/organizations/organization-routing.module.ts @@ -2,6 +2,7 @@ import { NgModule } from "@angular/core"; import { RouterModule, Routes } from "@angular/router"; import { authGuard } from "@bitwarden/angular/auth/guards"; +import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard"; import { canAccessOrgAdmin, canAccessGroupsTab, @@ -11,6 +12,7 @@ import { canAccessSettingsTab, } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { organizationPermissionsGuard } from "../../admin-console/organizations/guards/org-permissions.guard"; import { organizationRedirectGuard } from "../../admin-console/organizations/guards/org-redirect.guard"; @@ -18,6 +20,8 @@ import { OrganizationLayoutComponent } from "../../admin-console/organizations/l import { deepLinkGuard } from "../../auth/guards/deep-link.guard"; import { VaultModule } from "../../vault/org-vault/vault.module"; +import { isEnterpriseOrgGuard } from "./guards/is-enterprise-org.guard"; +import { AdminConsoleIntegrationsComponent } from "./integrations/integrations.component"; import { GroupsComponent } from "./manage/groups.component"; const routes: Routes = [ @@ -36,6 +40,17 @@ const routes: Routes = [ path: "vault", loadChildren: () => VaultModule, }, + { + path: "integrations", + canActivate: [ + canAccessFeature(FeatureFlag.PM14505AdminConsoleIntegrationPage), + isEnterpriseOrgGuard(false), + ], + component: AdminConsoleIntegrationsComponent, + data: { + titleId: "integrations", + }, + }, { path: "settings", loadChildren: () => diff --git a/apps/web/src/app/shared/components/index.ts b/apps/web/src/app/shared/components/index.ts new file mode 100644 index 00000000000..5745a7827ff --- /dev/null +++ b/apps/web/src/app/shared/components/index.ts @@ -0,0 +1,4 @@ +export * from "./integrations/integration-card/integration-card.component"; +export * from "./integrations/integration-grid/integration-grid.component"; +export * from "./integrations/integrations.pipe"; +export * from "./integrations/models"; diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integration-card/integration-card.component.html b/apps/web/src/app/shared/components/integrations/integration-card/integration-card.component.html similarity index 64% rename from bitwarden_license/bit-web/src/app/secrets-manager/integrations/integration-card/integration-card.component.html rename to apps/web/src/app/shared/components/integrations/integration-card/integration-card.component.html index 5bb9ed425fc..4801c392976 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integration-card/integration-card.component.html +++ b/apps/web/src/app/shared/components/integrations/integration-card/integration-card.component.html @@ -1,8 +1,11 @@
    +
    + +
    -

    {{ name }}

    +

    {{ name }}

    - {{ linkText }} {{ "new" | i18n }} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integration-card/integration-card.component.spec.ts b/apps/web/src/app/shared/components/integrations/integration-card/integration-card.component.spec.ts similarity index 86% rename from bitwarden_license/bit-web/src/app/secrets-manager/integrations/integration-card/integration-card.component.spec.ts rename to apps/web/src/app/shared/components/integrations/integration-card/integration-card.component.spec.ts index 94cec5f627f..c8b6a290427 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integration-card/integration-card.component.spec.ts +++ b/apps/web/src/app/shared/components/integrations/integration-card/integration-card.component.spec.ts @@ -1,9 +1,13 @@ import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { mock } from "jest-mock-extended"; import { BehaviorSubject } from "rxjs"; -import { SYSTEM_THEME_OBSERVABLE } from "../../../../../../../libs/angular/src/services/injection-tokens"; -import { ThemeType } from "../../../../../../../libs/common/src/platform/enums"; -import { ThemeStateService } from "../../../../../../../libs/common/src/platform/theming/theme-state.service"; +import { SYSTEM_THEME_OBSERVABLE } from "@bitwarden/angular/services/injection-tokens"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { ThemeType } from "@bitwarden/common/platform/enums"; +import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; +import { SharedModule } from "@bitwarden/components/src/shared"; +import { I18nPipe } from "@bitwarden/components/src/shared/i18n.pipe"; import { IntegrationCardComponent } from "./integration-card.component"; @@ -19,7 +23,7 @@ describe("IntegrationCardComponent", () => { systemTheme$.next(ThemeType.Light); await TestBed.configureTestingModule({ - declarations: [IntegrationCardComponent], + imports: [IntegrationCardComponent, SharedModule], providers: [ { provide: ThemeStateService, @@ -29,6 +33,14 @@ describe("IntegrationCardComponent", () => { provide: SYSTEM_THEME_OBSERVABLE, useValue: systemTheme$, }, + { + provide: I18nPipe, + useValue: mock(), + }, + { + provide: I18nService, + useValue: mock(), + }, ], }).compileComponents(); }); @@ -39,7 +51,6 @@ describe("IntegrationCardComponent", () => { component.name = "Integration Name"; component.image = "test-image.png"; - component.linkText = "Get started with integration"; component.linkURL = "https://example.com/"; fixture.detectChanges(); @@ -53,10 +64,8 @@ describe("IntegrationCardComponent", () => { it("renders card body", () => { const name = fixture.nativeElement.querySelector("h3"); - const link = fixture.nativeElement.querySelector("a"); expect(name.textContent).toBe("Integration Name"); - expect(link.textContent.trim()).toBe("Get started with integration"); }); it("assigns external rel attribute", () => { diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integration-card/integration-card.component.ts b/apps/web/src/app/shared/components/integrations/integration-card/integration-card.component.ts similarity index 95% rename from bitwarden_license/bit-web/src/app/secrets-manager/integrations/integration-card/integration-card.component.ts rename to apps/web/src/app/shared/components/integrations/integration-card/integration-card.component.ts index bf5f5bd3112..a33fbd28c96 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integration-card/integration-card.component.ts +++ b/apps/web/src/app/shared/components/integrations/integration-card/integration-card.component.ts @@ -13,9 +13,13 @@ import { SYSTEM_THEME_OBSERVABLE } from "@bitwarden/angular/services/injection-t import { ThemeType } from "@bitwarden/common/platform/enums"; import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; +import { SharedModule } from "../../../shared.module"; + @Component({ - selector: "sm-integration-card", + selector: "app-integration-card", templateUrl: "./integration-card.component.html", + standalone: true, + imports: [SharedModule], }) export class IntegrationCardComponent implements AfterViewInit, OnDestroy { private destroyed$: Subject = new Subject(); @@ -24,7 +28,6 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy { @Input() name: string; @Input() image: string; @Input() imageDarkMode?: string; - @Input() linkText: string; @Input() linkURL: string; /** Adds relevant `rel` attribute to external links */ diff --git a/apps/web/src/app/shared/components/integrations/integration-card/integration-card.stories.ts b/apps/web/src/app/shared/components/integrations/integration-card/integration-card.stories.ts new file mode 100644 index 00000000000..1d1e229740f --- /dev/null +++ b/apps/web/src/app/shared/components/integrations/integration-card/integration-card.stories.ts @@ -0,0 +1,63 @@ +import { Meta, StoryObj, moduleMetadata } from "@storybook/angular"; +import { of } from "rxjs"; + +import { SYSTEM_THEME_OBSERVABLE } from "@bitwarden/angular/services/injection-tokens"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { ThemeTypes } from "@bitwarden/common/platform/enums"; +import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; +import { I18nMockService } from "@bitwarden/components"; + +import { SharedModule } from "../../../shared.module"; + +import { IntegrationCardComponent } from "./integration-card.component"; + +class MockThemeService implements Partial {} + +export default { + title: "Web/Integration Layout/Integration Card", + component: IntegrationCardComponent, + decorators: [ + moduleMetadata({ + imports: [SharedModule], + providers: [ + { + provide: I18nService, + useFactory: () => { + return new I18nMockService({}); + }, + }, + { + provide: ThemeStateService, + useClass: MockThemeService, + }, + { + provide: SYSTEM_THEME_OBSERVABLE, + useValue: of(ThemeTypes.Light), + }, + ], + }), + ], + args: { + integrations: [], + }, +} as Meta; + +type Story = StoryObj; + +export const Default: Story = { + render: (args) => ({ + props: args, + template: /*html*/ ` + + `, + }), + args: { + name: "Bitwarden", + image: "/integrations/bitwarden-vertical-blue.svg", + linkURL: "https://bitwarden.com", + }, +}; diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integration-grid/integration-grid.component.html b/apps/web/src/app/shared/components/integrations/integration-grid/integration-grid.component.html similarity index 66% rename from bitwarden_license/bit-web/src/app/secrets-manager/integrations/integration-grid/integration-grid.component.html rename to apps/web/src/app/shared/components/integrations/integration-grid/integration-grid.component.html index a0c82d2f342..4b4b3ac972b 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integration-grid/integration-grid.component.html +++ b/apps/web/src/app/shared/components/integrations/integration-grid/integration-grid.component.html @@ -1,15 +1,18 @@
      -
    • - + + >
    diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integration-grid/integration-grid.component.spec.ts b/apps/web/src/app/shared/components/integrations/integration-grid/integration-grid.component.spec.ts similarity index 61% rename from bitwarden_license/bit-web/src/app/secrets-manager/integrations/integration-grid/integration-grid.component.spec.ts rename to apps/web/src/app/shared/components/integrations/integration-grid/integration-grid.component.spec.ts index e74e057e069..c77ec455e00 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integration-grid/integration-grid.component.spec.ts +++ b/apps/web/src/app/shared/components/integrations/integration-grid/integration-grid.component.spec.ts @@ -3,12 +3,16 @@ import { By } from "@angular/platform-browser"; import { mock } from "jest-mock-extended"; import { of } from "rxjs"; -import { SYSTEM_THEME_OBSERVABLE } from "../../../../../../../libs/angular/src/services/injection-tokens"; -import { IntegrationType } from "../../../../../../../libs/common/src/enums"; -import { ThemeType } from "../../../../../../../libs/common/src/platform/enums"; -import { ThemeStateService } from "../../../../../../../libs/common/src/platform/theming/theme-state.service"; +import { SYSTEM_THEME_OBSERVABLE } from "@bitwarden/angular/services/injection-tokens"; +import { IntegrationType } from "@bitwarden/common/enums"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { ThemeTypes } from "@bitwarden/common/platform/enums"; +import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; +import { SharedModule } from "@bitwarden/components/src/shared"; +import { I18nPipe } from "@bitwarden/components/src/shared/i18n.pipe"; + import { IntegrationCardComponent } from "../integration-card/integration-card.component"; -import { Integration } from "../models/integration"; +import { Integration } from "../models"; import { IntegrationGridComponent } from "./integration-grid.component"; @@ -19,14 +23,12 @@ describe("IntegrationGridComponent", () => { { name: "Integration 1", image: "test-image1.png", - linkText: "Get started with integration 1", linkURL: "https://example.com/1", type: IntegrationType.Integration, }, { name: "SDK 2", image: "test-image2.png", - linkText: "View SDK 2", linkURL: "https://example.com/2", type: IntegrationType.SDK, }, @@ -34,7 +36,7 @@ describe("IntegrationGridComponent", () => { beforeEach(() => { TestBed.configureTestingModule({ - declarations: [IntegrationGridComponent, IntegrationCardComponent], + imports: [IntegrationGridComponent, IntegrationCardComponent, SharedModule], providers: [ { provide: ThemeStateService, @@ -42,7 +44,15 @@ describe("IntegrationGridComponent", () => { }, { provide: SYSTEM_THEME_OBSERVABLE, - useValue: of(ThemeType.Light), + useValue: of(ThemeTypes.Light), + }, + { + provide: I18nPipe, + useValue: mock(), + }, + { + provide: I18nService, + useValue: mock({ t: (key, p1) => key + " " + p1 }), }, ], }); @@ -50,6 +60,8 @@ describe("IntegrationGridComponent", () => { fixture = TestBed.createComponent(IntegrationGridComponent); component = fixture.componentInstance; component.integrations = integrations; + component.ariaI18nKey = "integrationCardAriaLabel"; + component.tooltipI18nKey = "integrationCardTooltip"; fixture.detectChanges(); }); @@ -68,7 +80,6 @@ describe("IntegrationGridComponent", () => { expect(card.componentInstance.name).toBe("SDK 2"); expect(card.componentInstance.image).toBe("test-image2.png"); - expect(card.componentInstance.linkText).toBe("View SDK 2"); expect(card.componentInstance.linkURL).toBe("https://example.com/2"); }); @@ -78,4 +89,12 @@ describe("IntegrationGridComponent", () => { expect(card[0].componentInstance.externalURL).toBe(false); expect(card[1].componentInstance.externalURL).toBe(true); }); + + it("has a tool tip and aria label attributes", () => { + const card: HTMLElement = fixture.debugElement.queryAll(By.css("li"))[0].nativeElement; + expect(card.title).toBe("integrationCardTooltip" + " " + integrations[0].name); + expect(card.getAttribute("aria-label")).toBe( + "integrationCardAriaLabel" + " " + integrations[0].name, + ); + }); }); diff --git a/apps/web/src/app/shared/components/integrations/integration-grid/integration-grid.component.ts b/apps/web/src/app/shared/components/integrations/integration-grid/integration-grid.component.ts new file mode 100644 index 00000000000..7660162f879 --- /dev/null +++ b/apps/web/src/app/shared/components/integrations/integration-grid/integration-grid.component.ts @@ -0,0 +1,22 @@ +import { Component, Input } from "@angular/core"; + +import { IntegrationType } from "@bitwarden/common/enums"; + +import { SharedModule } from "../../../shared.module"; +import { IntegrationCardComponent } from "../integration-card/integration-card.component"; +import { Integration } from "../models"; + +@Component({ + selector: "app-integration-grid", + templateUrl: "./integration-grid.component.html", + standalone: true, + imports: [IntegrationCardComponent, SharedModule], +}) +export class IntegrationGridComponent { + @Input() integrations: Integration[]; + + @Input() ariaI18nKey: string = "integrationCardAriaLabel"; + @Input() tooltipI18nKey: string = "integrationCardTooltip"; + + protected IntegrationType = IntegrationType; +} diff --git a/apps/web/src/app/shared/components/integrations/integration-grid/integration-grid.stories.ts b/apps/web/src/app/shared/components/integrations/integration-grid/integration-grid.stories.ts new file mode 100644 index 00000000000..2ec0bccec3d --- /dev/null +++ b/apps/web/src/app/shared/components/integrations/integration-grid/integration-grid.stories.ts @@ -0,0 +1,77 @@ +import { Meta, StoryObj, moduleMetadata } from "@storybook/angular"; +import { of } from "rxjs"; + +import { SYSTEM_THEME_OBSERVABLE } from "@bitwarden/angular/services/injection-tokens"; +import { IntegrationType } from "@bitwarden/common/enums"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { ThemeTypes } from "@bitwarden/common/platform/enums"; +import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; +import { I18nMockService } from "@bitwarden/components"; + +import { SharedModule } from "../../../shared.module"; +import { IntegrationCardComponent } from "../integration-card/integration-card.component"; +import { IntegrationGridComponent } from "../integration-grid/integration-grid.component"; + +class MockThemeService implements Partial {} + +export default { + title: "Web/Integration Layout/Integration Grid", + component: IntegrationGridComponent, + decorators: [ + moduleMetadata({ + imports: [IntegrationCardComponent, SharedModule], + providers: [ + { + provide: I18nService, + useFactory: () => { + return new I18nMockService({ + integrationCardAriaLabel: "Go to integration", + integrationCardTooltip: "Go to integration", + }); + }, + }, + { + provide: ThemeStateService, + useClass: MockThemeService, + }, + { + provide: SYSTEM_THEME_OBSERVABLE, + useValue: of(ThemeTypes.Dark), + }, + ], + }), + ], +} as Meta; + +type Story = StoryObj; + +export const Default: Story = { + render: (args) => ({ + props: args, + template: /*html*/ ` + + `, + }), + args: { + integrations: [ + { + name: "Card 1", + linkURL: "https://bitwarden.com", + image: "/integrations/bitwarden-vertical-blue.svg", + type: IntegrationType.SSO, + }, + { + name: "Card 2", + linkURL: "https://bitwarden.com", + image: "/integrations/bitwarden-vertical-blue.svg", + type: IntegrationType.SDK, + }, + { + name: "Card 3", + linkURL: "https://bitwarden.com", + image: "/integrations/bitwarden-vertical-blue.svg", + type: IntegrationType.SCIM, + }, + ], + }, +}; diff --git a/apps/web/src/app/shared/components/integrations/integrations.pipe.ts b/apps/web/src/app/shared/components/integrations/integrations.pipe.ts new file mode 100644 index 00000000000..760d9913e9e --- /dev/null +++ b/apps/web/src/app/shared/components/integrations/integrations.pipe.ts @@ -0,0 +1,15 @@ +import { Pipe, PipeTransform } from "@angular/core"; + +import { IntegrationType } from "@bitwarden/common/enums"; + +import { Integration } from "../../../shared/components/integrations/models"; + +@Pipe({ + name: "filterIntegrations", + standalone: true, +}) +export class FilterIntegrationsPipe implements PipeTransform { + transform(integrations: Integration[], type: IntegrationType): Integration[] { + return integrations.filter((integration) => integration.type === type); + } +} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/integrations/models/integration.ts b/apps/web/src/app/shared/components/integrations/models.ts similarity index 95% rename from bitwarden_license/bit-web/src/app/secrets-manager/integrations/models/integration.ts rename to apps/web/src/app/shared/components/integrations/models.ts index 51ca79b30f7..765b1d44a2e 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/integrations/models/integration.ts +++ b/apps/web/src/app/shared/components/integrations/models.ts @@ -9,7 +9,6 @@ export type Integration = { */ imageDarkMode?: string; linkURL: string; - linkText: string; type: IntegrationType; /** * Shows the "New" badge until the defined date. diff --git a/apps/web/src/app/shared/index.ts b/apps/web/src/app/shared/index.ts index 7defcdedfda..f57648c0e40 100644 --- a/apps/web/src/app/shared/index.ts +++ b/apps/web/src/app/shared/index.ts @@ -1,2 +1,3 @@ export * from "./shared.module"; export * from "./loose-components.module"; +export * from "./components/index"; diff --git a/apps/web/src/images/integrations/aws-color.svg b/apps/web/src/images/integrations/aws-color.svg new file mode 100644 index 00000000000..963b65027db --- /dev/null +++ b/apps/web/src/images/integrations/aws-color.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web/src/images/integrations/aws-darkmode.svg b/apps/web/src/images/integrations/aws-darkmode.svg new file mode 100644 index 00000000000..64c9ba3cf94 --- /dev/null +++ b/apps/web/src/images/integrations/aws-darkmode.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web/src/images/integrations/azure-active-directory.svg b/apps/web/src/images/integrations/azure-active-directory.svg new file mode 100644 index 00000000000..22ea64f1f03 --- /dev/null +++ b/apps/web/src/images/integrations/azure-active-directory.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web/src/images/integrations/bitwarden-vertical-blue.svg b/apps/web/src/images/integrations/bitwarden-vertical-blue.svg new file mode 100644 index 00000000000..5d5200364d8 --- /dev/null +++ b/apps/web/src/images/integrations/bitwarden-vertical-blue.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web/src/images/integrations/jumpcloud-darkmode.svg b/apps/web/src/images/integrations/jumpcloud-darkmode.svg new file mode 100644 index 00000000000..6969fceeb84 --- /dev/null +++ b/apps/web/src/images/integrations/jumpcloud-darkmode.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web/src/images/integrations/logo-auth0-badge-color.svg b/apps/web/src/images/integrations/logo-auth0-badge-color.svg new file mode 100644 index 00000000000..24887cc7510 --- /dev/null +++ b/apps/web/src/images/integrations/logo-auth0-badge-color.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web/src/images/integrations/logo-duo-color.svg b/apps/web/src/images/integrations/logo-duo-color.svg new file mode 100644 index 00000000000..0959a215708 --- /dev/null +++ b/apps/web/src/images/integrations/logo-duo-color.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web/src/images/integrations/logo-elastic-badge-color.svg b/apps/web/src/images/integrations/logo-elastic-badge-color.svg new file mode 100644 index 00000000000..f6e00f3d40d --- /dev/null +++ b/apps/web/src/images/integrations/logo-elastic-badge-color.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web/src/images/integrations/logo-google-badge-color.svg b/apps/web/src/images/integrations/logo-google-badge-color.svg new file mode 100644 index 00000000000..c5a8fe50363 --- /dev/null +++ b/apps/web/src/images/integrations/logo-google-badge-color.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web/src/images/integrations/logo-jumpcloud-badge-color.svg b/apps/web/src/images/integrations/logo-jumpcloud-badge-color.svg new file mode 100644 index 00000000000..9349186d8a1 --- /dev/null +++ b/apps/web/src/images/integrations/logo-jumpcloud-badge-color.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web/src/images/integrations/logo-keycloak-icon.svg b/apps/web/src/images/integrations/logo-keycloak-icon.svg new file mode 100644 index 00000000000..862ffcb6c2b --- /dev/null +++ b/apps/web/src/images/integrations/logo-keycloak-icon.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/web/src/images/integrations/logo-microsoft-entra-id-color.svg b/apps/web/src/images/integrations/logo-microsoft-entra-id-color.svg new file mode 100644 index 00000000000..a6150c29c62 --- /dev/null +++ b/apps/web/src/images/integrations/logo-microsoft-entra-id-color.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web/src/images/integrations/logo-microsoft-intune-color.svg b/apps/web/src/images/integrations/logo-microsoft-intune-color.svg new file mode 100644 index 00000000000..2611cf4b3b8 --- /dev/null +++ b/apps/web/src/images/integrations/logo-microsoft-intune-color.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web/src/images/integrations/logo-microsoft-sentinel-color.svg b/apps/web/src/images/integrations/logo-microsoft-sentinel-color.svg new file mode 100644 index 00000000000..93135526c6f --- /dev/null +++ b/apps/web/src/images/integrations/logo-microsoft-sentinel-color.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web/src/images/integrations/logo-okta-symbol-black.svg b/apps/web/src/images/integrations/logo-okta-symbol-black.svg new file mode 100644 index 00000000000..876727ad56d --- /dev/null +++ b/apps/web/src/images/integrations/logo-okta-symbol-black.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web/src/images/integrations/logo-onelogin-badge-color.svg b/apps/web/src/images/integrations/logo-onelogin-badge-color.svg new file mode 100644 index 00000000000..e2d9ccbc0c1 --- /dev/null +++ b/apps/web/src/images/integrations/logo-onelogin-badge-color.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web/src/images/integrations/logo-panther-round-color.svg b/apps/web/src/images/integrations/logo-panther-round-color.svg new file mode 100644 index 00000000000..bed05507681 --- /dev/null +++ b/apps/web/src/images/integrations/logo-panther-round-color.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web/src/images/integrations/logo-ping-identity-badge-color.svg b/apps/web/src/images/integrations/logo-ping-identity-badge-color.svg new file mode 100644 index 00000000000..e34762c249c --- /dev/null +++ b/apps/web/src/images/integrations/logo-ping-identity-badge-color.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web/src/images/integrations/logo-rapid7-black.svg b/apps/web/src/images/integrations/logo-rapid7-black.svg new file mode 100644 index 00000000000..e2bb7a6f4a8 --- /dev/null +++ b/apps/web/src/images/integrations/logo-rapid7-black.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web/src/images/integrations/logo-splunk-black.svg b/apps/web/src/images/integrations/logo-splunk-black.svg new file mode 100644 index 00000000000..d25247bfca8 --- /dev/null +++ b/apps/web/src/images/integrations/logo-splunk-black.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web/src/images/integrations/okta-darkmode.svg b/apps/web/src/images/integrations/okta-darkmode.svg new file mode 100644 index 00000000000..e16e0d3c700 --- /dev/null +++ b/apps/web/src/images/integrations/okta-darkmode.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web/src/images/integrations/onelogin-darkmode.svg b/apps/web/src/images/integrations/onelogin-darkmode.svg new file mode 100644 index 00000000000..764b1684faa --- /dev/null +++ b/apps/web/src/images/integrations/onelogin-darkmode.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web/src/images/integrations/rapid7-darkmode.svg b/apps/web/src/images/integrations/rapid7-darkmode.svg new file mode 100644 index 00000000000..b5f25aae8bd --- /dev/null +++ b/apps/web/src/images/integrations/rapid7-darkmode.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web/src/images/integrations/splunk-darkmode.svg b/apps/web/src/images/integrations/splunk-darkmode.svg new file mode 100644 index 00000000000..a4515c0a18c --- /dev/null +++ b/apps/web/src/images/integrations/splunk-darkmode.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index cab0e703a7d..163c63a6d24 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -6831,6 +6831,10 @@ "message": "Automatically provision users and groups with your preferred identity provider via SCIM provisioning", "description": "the text, 'SCIM', is an acronym and should not be translated." }, + "scimIntegrationDescription": { + "message": "Automatically provision users and groups with your preferred identity provider via SCIM provisioning. Find supported integrations", + "description": "the text, 'SCIM', is an acronym and should not be translated." + }, "scimEnabledCheckboxDesc": { "message": "Enable SCIM", "description": "the text, 'SCIM', is an acronym and should not be translated." @@ -9025,44 +9029,103 @@ "sdksDesc": { "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." }, - "setUpGithubActions": { - "message": "Set up Github Actions" + "singleSignOn": { + "message": "Single sign-on" }, - "setUpKubernetes": { - "message": "Set up Kubernetes" + "ssoDescStart": { + "message": "Configure", + "description": "This represents the beginning of a sentence, broken up to include links. The full sentence will be 'Configure single sign-on for Bitwarden using the implementation guide for your Identity Provider." }, - "setUpGitlabCICD": { - "message": "Set up GitLab CI/CD" + "ssoDescEnd": { + "message": "for Bitwarden using the implementation guide for your Identity Provider.", + "description": "This represents the end of a sentence, broken up to include links. The full sentence will be 'Configure single sign-on for Bitwarden using the implementation guide for your Identity Provider." }, - "setUpAnsible": { - "message": "Set up Ansible" + "userProvisioning":{ + "message": "User provisioning" }, - "rustSDKRepo": { - "message": "View Rust repository" + "scimIntegration": { + "message": "SCIM" }, - "cSharpSDKRepo": { - "message": "View C# repository" + "scimIntegrationDescStart": { + "message": "Configure ", + "description": "This represents the beginning of a sentence, broken up to include links. The full sentence will be 'Configure SCIM (System for Cross-domain Identity Management) to automatically provision users and groups to Bitwarden using the implementation guide for your Identity Provider" }, - "cPlusPlusSDKRepo": { - "message": "View C++ repository" + "scimIntegrationDescEnd": { + "message": "(System for Cross-domain Identity Management) to automatically provision users and groups to Bitwarden using the implementation guide for your Identity Provider.", + "description": "This represents the end of a sentence, broken up to include links. The full sentence will be 'Configure SCIM (System for Cross-domain Identity Management) to automatically provision users and groups to Bitwarden using the implementation guide for your Identity Provider" }, - "jsWebAssemblySDKRepo": { - "message": "View JS WebAssembly repository" + "bwdc":{ + "message": "Bitwarden Directory Connector" }, - "javaSDKRepo": { - "message": "View Java repository" + "bwdcDesc": { + "message": "Configure Bitwarden Directory Connector to automatically provision users and groups using the implementation guide for your Identity Provider." }, - "pythonSDKRepo": { - "message": "View Python repository" + "eventManagement":{ + "message": "Event management" }, - "phpSDKRepo": { - "message": "View php repository" + "eventManagementDesc":{ + "message": "Integrate Bitwarden event logs with your SIEM (system information and event management) system by using the implementation guide for your platform." }, - "rubySDKRepo": { - "message": "View Ruby repository" + "deviceManagement":{ + "message": "Device management" }, - "goSDKRepo": { - "message": "View Go repository" + "deviceManagementDesc":{ + "message": "Configure device management for Bitwarden using the implementation guide for your platform." + + }, + "integrationCardTooltip":{ + "message": "Launch $INTEGRATION$ implementation guide.", + "placeholders": { + "integration": { + "content": "$1", + "example": "Google" + } + } + }, + "smIntegrationTooltip":{ + "message": "Set up $INTEGRATION$.", + "placeholders": { + "integration": { + "content": "$1", + "example": "Google" + } + } + }, + "smSdkTooltip":{ + "message": "View $SDK$ repository", + "placeholders": { + "sdk": { + "content": "$1", + "example": "Rust" + } + } + }, + "integrationCardAriaLabel":{ + "message": "open $INTEGRATION$ implementation guide in a new tab.", + "placeholders": { + "integration": { + "content": "$1", + "example": "google" + } + } + }, + "smSdkAriaLabel":{ + "message": "view $SDK$ repository in a new tab.", + "placeholders": { + "sdk": { + "content": "$1", + "example": "rust" + } + } + }, + "smIntegrationCardAriaLabel":{ + "message": "set up $INTEGRATION$ implementation guide in a new tab.", + "placeholders": { + "integration": { + "content": "$1", + "example": "google" + } + } }, "createNewClientToManageAsProvider": { "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." diff --git a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/scim.component.html b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/scim.component.html index e30883515e0..7ade2e6c63d 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/scim.component.html +++ b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/scim.component.html @@ -1,6 +1,11 @@ -

    {{ "scimDescription" | i18n }}

    +

    + {{ "scimIntegrationDescription" | i18n }} + +

    {{ "integrationsDesc" | i18n }}

    - +
    @@ -12,5 +16,9 @@ {{ "sdks" | i18n }}

    {{ "sdksDesc" | i18n }}

    - + diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.spec.ts b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.spec.ts index 6c8ea28bc2f..e4a65f7ddd8 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.spec.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.spec.ts @@ -4,14 +4,17 @@ import { By } from "@angular/platform-browser"; import { mock } from "jest-mock-extended"; import { of } from "rxjs"; +import { SharedModule } from "@bitwarden/components/src/shared"; +import { + IntegrationCardComponent, + IntegrationGridComponent, +} from "@bitwarden/web-vault/app/shared"; + import { SYSTEM_THEME_OBSERVABLE } from "../../../../../../libs/angular/src/services/injection-tokens"; import { I18nService } from "../../../../../../libs/common/src/platform/abstractions/i18n.service"; import { ThemeType } from "../../../../../../libs/common/src/platform/enums"; import { ThemeStateService } from "../../../../../../libs/common/src/platform/theming/theme-state.service"; -import { I18nPipe } from "../../../../../../libs/components/src/shared/i18n.pipe"; -import { IntegrationCardComponent } from "./integration-card/integration-card.component"; -import { IntegrationGridComponent } from "./integration-grid/integration-grid.component"; import { IntegrationsComponent } from "./integrations.component"; @Component({ @@ -31,18 +34,12 @@ describe("IntegrationsComponent", () => { beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [ - IntegrationsComponent, - IntegrationGridComponent, - IntegrationCardComponent, - MockHeaderComponent, - MockNewMenuComponent, - I18nPipe, - ], + declarations: [IntegrationsComponent, MockHeaderComponent, MockNewMenuComponent], + imports: [IntegrationGridComponent, IntegrationCardComponent, SharedModule], providers: [ { provide: I18nService, - useValue: mock({ t: (key) => key }), + useValue: mock(), }, { provide: ThemeStateService, diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.ts index 9e846d45034..b8f9386d715 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.ts @@ -1,9 +1,7 @@ import { Component } from "@angular/core"; import { IntegrationType } from "@bitwarden/common/enums"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; - -import { Integration } from "./models/integration"; +import { Integration } from "@bitwarden/web-vault/app/shared"; @Component({ selector: "sm-integrations", @@ -12,11 +10,10 @@ import { Integration } from "./models/integration"; export class IntegrationsComponent { private integrationsAndSdks: Integration[] = []; - constructor(i18nService: I18nService) { + constructor() { this.integrationsAndSdks = [ { name: "Rust", - linkText: i18nService.t("rustSDKRepo"), linkURL: "https://github.com/bitwarden/sdk", image: "../../../../../../../images/secrets-manager/sdks/rust.svg", imageDarkMode: "../../../../../../../images/secrets-manager/sdks/rust-white.svg", @@ -24,7 +21,6 @@ export class IntegrationsComponent { }, { name: "GitHub Actions", - linkText: i18nService.t("setUpGithubActions"), linkURL: "https://bitwarden.com/help/github-actions-integration/", image: "../../../../../../../images/secrets-manager/integrations/github.svg", imageDarkMode: "../../../../../../../images/secrets-manager/integrations/github-white.svg", @@ -32,7 +28,6 @@ export class IntegrationsComponent { }, { name: "GitLab CI/CD", - linkText: i18nService.t("setUpGitlabCICD"), linkURL: "https://bitwarden.com/help/gitlab-integration/", image: "../../../../../../../images/secrets-manager/integrations/gitlab.svg", imageDarkMode: "../../../../../../../images/secrets-manager/integrations/gitlab-white.svg", @@ -40,35 +35,30 @@ export class IntegrationsComponent { }, { name: "Ansible", - linkText: i18nService.t("setUpAnsible"), linkURL: "https://bitwarden.com/help/ansible-integration/", image: "../../../../../../../images/secrets-manager/integrations/ansible.svg", type: IntegrationType.Integration, }, { name: "C#", - linkText: i18nService.t("cSharpSDKRepo"), linkURL: "https://github.com/bitwarden/sdk/tree/main/languages/csharp", image: "../../../../../../../images/secrets-manager/sdks/c-sharp.svg", type: IntegrationType.SDK, }, { name: "C++", - linkText: i18nService.t("cPlusPlusSDKRepo"), linkURL: "https://github.com/bitwarden/sdk/tree/main/languages/cpp", image: "../../../../../../../images/secrets-manager/sdks/c-plus-plus.png", type: IntegrationType.SDK, }, { name: "Go", - linkText: i18nService.t("goSDKRepo"), linkURL: "https://github.com/bitwarden/sdk/tree/main/languages/go", image: "../../../../../../../images/secrets-manager/sdks/go.svg", type: IntegrationType.SDK, }, { name: "Java", - linkText: i18nService.t("javaSDKRepo"), linkURL: "https://github.com/bitwarden/sdk/tree/main/languages/java", image: "../../../../../../../images/secrets-manager/sdks/java.svg", imageDarkMode: "../../../../../../../images/secrets-manager/sdks/java-white.svg", @@ -76,35 +66,30 @@ export class IntegrationsComponent { }, { name: "JS WebAssembly", - linkText: i18nService.t("jsWebAssemblySDKRepo"), linkURL: "https://github.com/bitwarden/sdk/tree/main/languages/js", image: "../../../../../../../images/secrets-manager/sdks/wasm.svg", type: IntegrationType.SDK, }, { name: "php", - linkText: i18nService.t("phpSDKRepo"), linkURL: "https://github.com/bitwarden/sdk/tree/main/languages/php", image: "../../../../../../../images/secrets-manager/sdks/php.svg", type: IntegrationType.SDK, }, { name: "Python", - linkText: i18nService.t("pythonSDKRepo"), linkURL: "https://github.com/bitwarden/sdk/tree/main/languages/python", image: "../../../../../../../images/secrets-manager/sdks/python.svg", type: IntegrationType.SDK, }, { name: "Ruby", - linkText: i18nService.t("rubySDKRepo"), linkURL: "https://github.com/bitwarden/sdk/tree/main/languages/ruby", image: "../../../../../../../images/secrets-manager/sdks/ruby.png", type: IntegrationType.SDK, }, { name: "Kubernetes Operator", - linkText: i18nService.t("setUpKubernetes"), linkURL: "https://bitwarden.com/help/secrets-manager-kubernetes-operator/", image: "../../../../../../../images/secrets-manager/integrations/kubernetes.svg", type: IntegrationType.Integration, diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.module.ts b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.module.ts index 0d26b626f16..b79892f5ed6 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.module.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.module.ts @@ -1,15 +1,22 @@ import { NgModule } from "@angular/core"; +import { + IntegrationCardComponent, + IntegrationGridComponent, +} from "@bitwarden/web-vault/app/shared"; + import { SecretsManagerSharedModule } from "../shared/sm-shared.module"; -import { IntegrationCardComponent } from "./integration-card/integration-card.component"; -import { IntegrationGridComponent } from "./integration-grid/integration-grid.component"; import { IntegrationsRoutingModule } from "./integrations-routing.module"; import { IntegrationsComponent } from "./integrations.component"; @NgModule({ - imports: [SecretsManagerSharedModule, IntegrationsRoutingModule], - declarations: [IntegrationsComponent, IntegrationGridComponent, IntegrationCardComponent], - providers: [], + imports: [ + SecretsManagerSharedModule, + IntegrationsRoutingModule, + IntegrationCardComponent, + IntegrationGridComponent, + ], + declarations: [IntegrationsComponent], }) export class IntegrationsModule {} diff --git a/bitwarden_license/bit-web/tsconfig.json b/bitwarden_license/bit-web/tsconfig.json index 3ccdade273e..09de92d355d 100644 --- a/bitwarden_license/bit-web/tsconfig.json +++ b/bitwarden_license/bit-web/tsconfig.json @@ -46,6 +46,7 @@ "../../apps/web/src/**/*.spec.ts", "../../libs/common/src/platform/services/**/*.worker.ts", - "src/**/*.stories.ts" + "src/**/*.stories.ts", + "src/**/*.spec.ts" ] } diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index f9630aba04f..96283b0000d 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -32,6 +32,7 @@ export enum FeatureFlag { VerifiedSsoDomainEndpoint = "pm-12337-refactor-sso-details-endpoint", PM11901_RefactorSelfHostingLicenseUploader = "PM-11901-refactor-self-hosting-license-uploader", AccessIntelligence = "pm-13227-access-intelligence", + PM14505AdminConsoleIntegrationPage = "pm-14505-admin-console-integration-page", CriticalApps = "pm-14466-risk-insights-critical-application", TrialPaymentOptional = "PM-8163-trial-payment", SecurityTasks = "security-tasks", @@ -81,6 +82,7 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.VerifiedSsoDomainEndpoint]: FALSE, [FeatureFlag.PM11901_RefactorSelfHostingLicenseUploader]: FALSE, [FeatureFlag.AccessIntelligence]: FALSE, + [FeatureFlag.PM14505AdminConsoleIntegrationPage]: FALSE, [FeatureFlag.CriticalApps]: FALSE, [FeatureFlag.TrialPaymentOptional]: FALSE, [FeatureFlag.SecurityTasks]: FALSE, diff --git a/libs/common/src/enums/integration-type.enum.ts b/libs/common/src/enums/integration-type.enum.ts index acb95106976..42c385fe715 100644 --- a/libs/common/src/enums/integration-type.enum.ts +++ b/libs/common/src/enums/integration-type.enum.ts @@ -1,4 +1,9 @@ export enum IntegrationType { Integration = "integration", SDK = "sdk", + SSO = "sso", + SCIM = "scim", + BWDC = "bwdc", + EVENT = "event", + DEVICE = "device", } From d6e1fe70cab98ab091322f0316fc6bbcb54c5c65 Mon Sep 17 00:00:00 2001 From: Vijay Oommen Date: Thu, 5 Dec 2024 11:24:51 -0600 Subject: [PATCH 33/46] PM-15091 Remove client side featureflag.AccessIntelligence and use DB feature flag (#12247) * PM-15091 remove featureflag.AccessIntelligence * removed unwanted lines of code * fixed merge conflict --- .../organizations/layouts/organization-layout.component.html | 2 +- .../organizations/layouts/organization-layout.component.ts | 4 ---- .../access-intelligence-routing.module.ts | 5 ++--- .../src/admin-console/models/data/organization.data.spec.ts | 1 + .../src/admin-console/models/data/organization.data.ts | 2 ++ libs/common/src/admin-console/models/domain/organization.ts | 2 ++ .../admin-console/models/response/organization.response.ts | 2 ++ .../models/response/profile-organization.response.ts | 2 ++ libs/common/src/enums/feature-flag.enum.ts | 2 -- 9 files changed, 12 insertions(+), 10 deletions(-) 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 ce832ef06e9..fa4d027d0f6 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 @@ -4,7 +4,7 @@ p.organizationId), switchMap((id) => this.organizationService.organizations$.pipe(getById(id))), diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/access-intelligence-routing.module.ts b/bitwarden_license/bit-web/src/app/tools/access-intelligence/access-intelligence-routing.module.ts index c13cc0efae8..993d9c0a134 100644 --- a/bitwarden_license/bit-web/src/app/tools/access-intelligence/access-intelligence-routing.module.ts +++ b/bitwarden_license/bit-web/src/app/tools/access-intelligence/access-intelligence-routing.module.ts @@ -1,15 +1,14 @@ import { NgModule } from "@angular/core"; import { RouterModule, Routes } from "@angular/router"; -import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { organizationPermissionsGuard } from "@bitwarden/web-vault/app/admin-console/organizations/guards/org-permissions.guard"; import { RiskInsightsComponent } from "./risk-insights.component"; const routes: Routes = [ { path: "risk-insights", - canActivate: [canAccessFeature(FeatureFlag.AccessIntelligence)], + canActivate: [organizationPermissionsGuard((org) => org.useRiskInsights)], component: RiskInsightsComponent, data: { titleId: "RiskInsights", diff --git a/libs/common/src/admin-console/models/data/organization.data.spec.ts b/libs/common/src/admin-console/models/data/organization.data.spec.ts index 12c5339a245..da9a82e7c5c 100644 --- a/libs/common/src/admin-console/models/data/organization.data.spec.ts +++ b/libs/common/src/admin-console/models/data/organization.data.spec.ts @@ -56,6 +56,7 @@ describe("ORGANIZATIONS state", () => { allowAdminAccessToAllCollectionItems: false, familySponsorshipLastSyncDate: new Date(), userIsManagedByOrganization: false, + useRiskInsights: false, }, }; const result = sut.deserializer(JSON.parse(JSON.stringify(expectedResult))); diff --git a/libs/common/src/admin-console/models/data/organization.data.ts b/libs/common/src/admin-console/models/data/organization.data.ts index d8032fd6fcf..23a0f360f9e 100644 --- a/libs/common/src/admin-console/models/data/organization.data.ts +++ b/libs/common/src/admin-console/models/data/organization.data.ts @@ -56,6 +56,7 @@ export class OrganizationData { limitCollectionDeletion: boolean; allowAdminAccessToAllCollectionItems: boolean; userIsManagedByOrganization: boolean; + useRiskInsights: boolean; constructor( response?: ProfileOrganizationResponse, @@ -116,6 +117,7 @@ export class OrganizationData { this.limitCollectionDeletion = response.limitCollectionDeletion; this.allowAdminAccessToAllCollectionItems = response.allowAdminAccessToAllCollectionItems; this.userIsManagedByOrganization = response.userIsManagedByOrganization; + this.useRiskInsights = response.useRiskInsights; this.isMember = options.isMember; this.isProviderUser = options.isProviderUser; diff --git a/libs/common/src/admin-console/models/domain/organization.ts b/libs/common/src/admin-console/models/domain/organization.ts index 497d3b0889b..3f3ec0f4457 100644 --- a/libs/common/src/admin-console/models/domain/organization.ts +++ b/libs/common/src/admin-console/models/domain/organization.ts @@ -81,6 +81,7 @@ export class Organization { * matches one of the verified domains of that organization, and the user is a member of it. */ userIsManagedByOrganization: boolean; + useRiskInsights: boolean; constructor(obj?: OrganizationData) { if (obj == null) { @@ -137,6 +138,7 @@ export class Organization { this.limitCollectionDeletion = obj.limitCollectionDeletion; this.allowAdminAccessToAllCollectionItems = obj.allowAdminAccessToAllCollectionItems; this.userIsManagedByOrganization = obj.userIsManagedByOrganization; + this.useRiskInsights = obj.useRiskInsights; } get canAccess() { diff --git a/libs/common/src/admin-console/models/response/organization.response.ts b/libs/common/src/admin-console/models/response/organization.response.ts index e033646797d..fd460896a31 100644 --- a/libs/common/src/admin-console/models/response/organization.response.ts +++ b/libs/common/src/admin-console/models/response/organization.response.ts @@ -35,6 +35,7 @@ export class OrganizationResponse extends BaseResponse { limitCollectionCreation: boolean; limitCollectionDeletion: boolean; allowAdminAccessToAllCollectionItems: boolean; + useRiskInsights: boolean; constructor(response: any) { super(response); @@ -75,5 +76,6 @@ export class OrganizationResponse extends BaseResponse { this.allowAdminAccessToAllCollectionItems = this.getResponseProperty( "AllowAdminAccessToAllCollectionItems", ); + this.useRiskInsights = this.getResponseProperty("UseRiskInsights"); } } diff --git a/libs/common/src/admin-console/models/response/profile-organization.response.ts b/libs/common/src/admin-console/models/response/profile-organization.response.ts index ed1f45d4917..9c4b8885ab8 100644 --- a/libs/common/src/admin-console/models/response/profile-organization.response.ts +++ b/libs/common/src/admin-console/models/response/profile-organization.response.ts @@ -53,6 +53,7 @@ export class ProfileOrganizationResponse extends BaseResponse { limitCollectionDeletion: boolean; allowAdminAccessToAllCollectionItems: boolean; userIsManagedByOrganization: boolean; + useRiskInsights: boolean; constructor(response: any) { super(response); @@ -117,5 +118,6 @@ export class ProfileOrganizationResponse extends BaseResponse { "AllowAdminAccessToAllCollectionItems", ); this.userIsManagedByOrganization = this.getResponseProperty("UserIsManagedByOrganization"); + this.useRiskInsights = this.getResponseProperty("UseRiskInsights"); } } diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index 96283b0000d..66d6b155e90 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 { CipherKeyEncryption = "cipher-key-encryption", VerifiedSsoDomainEndpoint = "pm-12337-refactor-sso-details-endpoint", PM11901_RefactorSelfHostingLicenseUploader = "PM-11901-refactor-self-hosting-license-uploader", - AccessIntelligence = "pm-13227-access-intelligence", PM14505AdminConsoleIntegrationPage = "pm-14505-admin-console-integration-page", CriticalApps = "pm-14466-risk-insights-critical-application", TrialPaymentOptional = "PM-8163-trial-payment", @@ -81,7 +80,6 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.CipherKeyEncryption]: FALSE, [FeatureFlag.VerifiedSsoDomainEndpoint]: FALSE, [FeatureFlag.PM11901_RefactorSelfHostingLicenseUploader]: FALSE, - [FeatureFlag.AccessIntelligence]: FALSE, [FeatureFlag.PM14505AdminConsoleIntegrationPage]: FALSE, [FeatureFlag.CriticalApps]: FALSE, [FeatureFlag.TrialPaymentOptional]: FALSE, From 8d68a2dd58d7cc79c2c5440ce99b7d593d4e9393 Mon Sep 17 00:00:00 2001 From: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com> Date: Thu, 5 Dec 2024 20:22:13 -0500 Subject: [PATCH 34/46] Auth/PM-13659 - 2FA Timeout - Attempted Fix (#12263) fix(auth): attempt to resolve 2FA session timeout issue --- .../src/service-container/service-container.ts | 7 +++++++ libs/angular/src/services/injection-tokens.ts | 6 +++++- .../src/services/jslib-services.module.ts | 9 ++++++++- .../login-strategies/login-strategy.service.ts | 17 +++++++++++------ 4 files changed, 31 insertions(+), 8 deletions(-) diff --git a/apps/cli/src/service-container/service-container.ts b/apps/cli/src/service-container/service-container.ts index 21e8f9f2082..ce944db78ff 100644 --- a/apps/cli/src/service-container/service-container.ts +++ b/apps/cli/src/service-container/service-container.ts @@ -17,6 +17,7 @@ import { PinService, PinServiceAbstraction, UserDecryptionOptionsService, + Executor, } from "@bitwarden/auth/common"; import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service"; import { EventUploadService as EventUploadServiceAbstraction } from "@bitwarden/common/abstractions/event/event-upload.service"; @@ -614,6 +615,11 @@ export class ServiceContainer { this.configService, ); + // Execute any authn session timeout logic without any wrapping logic. + // An executor is required to ensure the logic is executed in an Angular context when it + // it is available. + const authnSessionTimeoutExecutor: Executor = (fn) => fn(); + this.loginStrategyService = new LoginStrategyService( this.accountService, this.masterPasswordService, @@ -640,6 +646,7 @@ export class ServiceContainer { this.vaultTimeoutSettingsService, this.kdfConfigService, this.taskSchedulerService, + authnSessionTimeoutExecutor, ); // FIXME: CLI does not support autofill diff --git a/libs/angular/src/services/injection-tokens.ts b/libs/angular/src/services/injection-tokens.ts index 86c5642a0c4..69a1ed3f613 100644 --- a/libs/angular/src/services/injection-tokens.ts +++ b/libs/angular/src/services/injection-tokens.ts @@ -1,7 +1,7 @@ import { InjectionToken } from "@angular/core"; import { Observable, Subject } from "rxjs"; -import { LogoutReason } from "@bitwarden/auth/common"; +import { Executor, LogoutReason } from "@bitwarden/auth/common"; import { ClientType } from "@bitwarden/common/enums"; import { RegionConfig } from "@bitwarden/common/platform/abstractions/environment.service"; import { @@ -68,3 +68,7 @@ export const REFRESH_ACCESS_TOKEN_ERROR_CALLBACK = new SafeInjectionToken<() => export const ENV_ADDITIONAL_REGIONS = new SafeInjectionToken( "ENV_ADDITIONAL_REGIONS", ); + +export const AUTHN_SESSION_TIMEOUT_EXECUTOR = new SafeInjectionToken( + "AuthnSessionTimeoutExecutor", +); diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index a43f1fa07a8..8637e26a2b2 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -1,4 +1,4 @@ -import { ErrorHandler, LOCALE_ID, NgModule } from "@angular/core"; +import { ErrorHandler, LOCALE_ID, NgModule, NgZone } from "@angular/core"; import { Subject } from "rxjs"; import { @@ -319,6 +319,7 @@ import { CLIENT_TYPE, REFRESH_ACCESS_TOKEN_ERROR_CALLBACK, ENV_ADDITIONAL_REGIONS, + AUTHN_SESSION_TIMEOUT_EXECUTOR, } from "./injection-tokens"; import { ModalService } from "./modal.service"; @@ -411,6 +412,11 @@ const safeProviders: SafeProvider[] = [ TokenServiceAbstraction, ], }), + safeProvider({ + provide: AUTHN_SESSION_TIMEOUT_EXECUTOR, + useFactory: (ngZone: NgZone) => (fn: () => void) => ngZone.run(fn), + deps: [NgZone], + }), safeProvider({ provide: LoginStrategyServiceAbstraction, useClass: LoginStrategyService, @@ -440,6 +446,7 @@ const safeProviders: SafeProvider[] = [ VaultTimeoutSettingsServiceAbstraction, KdfConfigService, TaskSchedulerService, + AUTHN_SESSION_TIMEOUT_EXECUTOR, ], }), safeProvider({ diff --git a/libs/auth/src/common/services/login-strategies/login-strategy.service.ts b/libs/auth/src/common/services/login-strategies/login-strategy.service.ts index 99e3c057e11..1d5001f1f06 100644 --- a/libs/auth/src/common/services/login-strategies/login-strategy.service.ts +++ b/libs/auth/src/common/services/login-strategies/login-strategy.service.ts @@ -71,6 +71,8 @@ import { const sessionTimeoutLength = 5 * 60 * 1000; // 5 minutes +export type Executor = (fn: () => void) => void; + export class LoginStrategyService implements LoginStrategyServiceAbstraction { private sessionTimeoutSubscription: Subscription; private currentAuthnTypeState: GlobalState; @@ -118,6 +120,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { protected vaultTimeoutSettingsService: VaultTimeoutSettingsService, protected kdfConfigService: KdfConfigService, protected taskSchedulerService: TaskSchedulerService, + private authnSessionTimeoutExecutor: Executor = (fn) => fn(), // Default to no-op ) { this.currentAuthnTypeState = this.stateProvider.get(CURRENT_LOGIN_STRATEGY_KEY); this.loginStrategyCacheState = this.stateProvider.get(CACHE_KEY); @@ -128,12 +131,14 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { this.taskSchedulerService.registerTaskHandler( ScheduledTaskNames.loginStrategySessionTimeout, async () => { - this.twoFactorTimeoutSubject.next(true); - try { - await this.clearCache(); - } catch (e) { - this.logService.error("Failed to clear cache during session timeout", e); - } + this.authnSessionTimeoutExecutor(async () => { + this.twoFactorTimeoutSubject.next(true); + try { + await this.clearCache(); + } catch (e) { + this.logService.error("Failed to clear cache during session timeout", e); + } + }); }, ); From f95cc7b82cb0e8f1f5921740c4033e2ddbcbd536 Mon Sep 17 00:00:00 2001 From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Date: Fri, 6 Dec 2024 15:17:48 +0100 Subject: [PATCH 35/46] Resolve the Unauthorized issue (#12262) --- apps/web/src/app/vault/individual-vault/vault.component.ts | 5 ++++- apps/web/src/app/vault/org-vault/vault.component.ts | 4 +++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/apps/web/src/app/vault/individual-vault/vault.component.ts b/apps/web/src/app/vault/individual-vault/vault.component.ts index 5f66bb49af5..73a5de2ad5e 100644 --- a/apps/web/src/app/vault/individual-vault/vault.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault.component.ts @@ -210,7 +210,10 @@ export class VaultComponent implements OnInit, OnDestroy { protected organizationsPaymentStatus$: Observable = combineLatest([ this.organizationService.organizations$.pipe( - map((organizations) => organizations?.filter((org) => org.isOwner) ?? []), + map( + (organizations) => + organizations?.filter((org) => org.isOwner && org.canViewBillingHistory) ?? [], + ), ), this.hasSubscription$, ]).pipe( diff --git a/apps/web/src/app/vault/org-vault/vault.component.ts b/apps/web/src/app/vault/org-vault/vault.component.ts index 18cc6e49abc..eb7d586cf74 100644 --- a/apps/web/src/app/vault/org-vault/vault.component.ts +++ b/apps/web/src/app/vault/org-vault/vault.component.ts @@ -592,7 +592,9 @@ export class VaultComponent implements OnInit, OnDestroy { organization$, this.hasSubscription$.pipe(filter((hasSubscription) => hasSubscription !== null)), ]).pipe( - filter(([org, hasSubscription]) => org.isOwner && hasSubscription), + filter( + ([org, hasSubscription]) => org.isOwner && hasSubscription && org.canViewBillingHistory, + ), switchMap(([org]) => combineLatest([ of(org), From f16bfa4cd2136d55d1cfd4d4f4e63b8d364bea2a Mon Sep 17 00:00:00 2001 From: Andreas Coroiu Date: Fri, 6 Dec 2024 16:31:30 +0100 Subject: [PATCH 36/46] [PM-9035] desktop build logic to provide credentials to os on sync (#10181) * feat: scaffold desktop_objc * feat: rename fido2 to autofill * feat: scaffold electron autofill * feat: auto call hello world on init * feat: scaffold call to basic objc function * feat: simple log that checks if autofill is enabled * feat: adding some availability guards * feat: scaffold services and allow calls from inspector * feat: create custom type for returning strings across rust/objc boundary * chore: clean up comments * feat: enable ARC * feat: add util function `c_string_to_nsstring` * chore: refactor and rename to `run_command` * feat: add try-catch around command execution * feat: properly implement command calling Add static typing. Add proper error handling. * feat: add autoreleasepool to avoid memory leaks * chore: change objc names to camelCase * fix: error returning * feat: extract some helper functions into utils class * feat: scaffold status command * feat: implement status command * feat: implement password credential mapping * wip: implement sync command This crashes because we are not properly handling the fact that `saveCredentialIdentities` uses callbacks, resulting in a race condition where we try to access a variable (result) that has already gotten dealloc'd. * feat: first version of callback * feat: make run_command async * feat: functioning callback returns * chore: refactor to make objc code easier to read and use * feat: refactor everything to use new callback return method * feat: re-implement status command with callback * fix: warning about CommandContext not being FFI-safe * feat: implement sync command using callbacks * feat: implement manual password credential sync * feat: add auto syncing * docs: add todo * feat: add support for passkeys * chore: move desktop autofill service to init service * feat: auto-add all .m files to builder * fix: native build on unix and windows * fix: unused compiler warnings * fix: napi type exports * feat: add corresponding dist command * feat: comment signing profile until we fix signing * fix: build breaking on non-macOS platforms * chore: cargo lock update * chore: revert accidental version change * feat: put sync behind feature flag * chore: put files in autofill folder * fix: obj-c code not recompiling on changes * feat: add `namespace` to commands * fix: linting complaining about flag * feat: add autofill as owner of their objc code * chore: make autofill owner of run_command in core crate * fix: re-add napi annotation * fix: remove dev bypass --- .github/CODEOWNERS | 2 + apps/desktop/desktop_native/Cargo.lock | 245 ++++++++++++------ apps/desktop/desktop_native/core/Cargo.toml | 11 +- .../desktop_native/core/src/autofill/macos.rs | 5 + .../desktop_native/core/src/autofill/mod.rs | 5 + .../desktop_native/core/src/autofill/unix.rs | 5 + .../core/src/autofill/windows.rs | 5 + apps/desktop/desktop_native/core/src/lib.rs | 6 +- apps/desktop/desktop_native/napi/index.d.ts | 3 + apps/desktop/desktop_native/napi/src/lib.rs | 57 +++- apps/desktop/desktop_native/objc/Cargo.toml | 21 ++ apps/desktop/desktop_native/objc/build.rs | 22 ++ apps/desktop/desktop_native/objc/src/lib.rs | 124 +++++++++ .../desktop_native/objc/src/native/.clangd | 2 + .../src/native/autofill/commands/status.h | 8 + .../src/native/autofill/commands/status.m | 57 ++++ .../objc/src/native/autofill/commands/sync.h | 8 + .../objc/src/native/autofill/commands/sync.m | 59 +++++ .../native/autofill/run_autofill_command.h | 8 + .../native/autofill/run_autofill_command.m | 20 ++ .../desktop_native/objc/src/native/interop.h | 47 ++++ .../desktop_native/objc/src/native/interop.m | 71 +++++ .../objc/src/native/run_command.m | 39 +++ .../desktop_native/objc/src/native/utils.h | 11 + .../desktop_native/objc/src/native/utils.m | 28 ++ .../CredentialProviderViewController.swift | 21 +- apps/desktop/package.json | 1 + apps/desktop/scripts/build-macos-extension.js | 5 +- apps/desktop/src/app/services/init.service.ts | 4 + .../src/app/services/services.module.ts | 6 + apps/desktop/src/autofill/preload.ts | 9 + .../services/desktop-autofill.service.ts | 121 +++++++++ apps/desktop/src/main.ts | 5 + .../src/platform/main/autofill/command.ts | 23 ++ .../main/autofill/native-autofill.main.ts | 53 ++++ .../platform/main/autofill/status.command.ts | 20 ++ .../platform/main/autofill/sync.command.ts | 37 +++ apps/desktop/src/preload.ts | 2 + libs/common/src/enums/feature-flag.enum.ts | 2 + .../services/fido2/fido2-autofill-utils.ts | 26 ++ .../view/fido2-credential-autofill.view.ts | 7 + 41 files changed, 1099 insertions(+), 112 deletions(-) create mode 100644 apps/desktop/desktop_native/core/src/autofill/macos.rs create mode 100644 apps/desktop/desktop_native/core/src/autofill/mod.rs create mode 100644 apps/desktop/desktop_native/core/src/autofill/unix.rs create mode 100644 apps/desktop/desktop_native/core/src/autofill/windows.rs create mode 100644 apps/desktop/desktop_native/objc/Cargo.toml create mode 100644 apps/desktop/desktop_native/objc/build.rs create mode 100644 apps/desktop/desktop_native/objc/src/lib.rs create mode 100644 apps/desktop/desktop_native/objc/src/native/.clangd create mode 100644 apps/desktop/desktop_native/objc/src/native/autofill/commands/status.h create mode 100644 apps/desktop/desktop_native/objc/src/native/autofill/commands/status.m create mode 100644 apps/desktop/desktop_native/objc/src/native/autofill/commands/sync.h create mode 100644 apps/desktop/desktop_native/objc/src/native/autofill/commands/sync.m create mode 100644 apps/desktop/desktop_native/objc/src/native/autofill/run_autofill_command.h create mode 100644 apps/desktop/desktop_native/objc/src/native/autofill/run_autofill_command.m create mode 100644 apps/desktop/desktop_native/objc/src/native/interop.h create mode 100644 apps/desktop/desktop_native/objc/src/native/interop.m create mode 100644 apps/desktop/desktop_native/objc/src/native/run_command.m create mode 100644 apps/desktop/desktop_native/objc/src/native/utils.h create mode 100644 apps/desktop/desktop_native/objc/src/native/utils.m create mode 100644 apps/desktop/src/autofill/preload.ts create mode 100644 apps/desktop/src/autofill/services/desktop-autofill.service.ts create mode 100644 apps/desktop/src/platform/main/autofill/command.ts create mode 100644 apps/desktop/src/platform/main/autofill/native-autofill.main.ts create mode 100644 apps/desktop/src/platform/main/autofill/status.command.ts create mode 100644 apps/desktop/src/platform/main/autofill/sync.command.ts create mode 100644 libs/common/src/platform/services/fido2/fido2-autofill-utils.ts create mode 100644 libs/common/src/vault/models/view/fido2-credential-autofill.view.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 12a8c182335..959c176c5cf 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -103,6 +103,8 @@ apps/web/src/app/layouts @bitwarden/team-design-system ## Desktop native module ## 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 ## Key management team files ## apps/desktop/src/key-management @bitwarden/team-key-management-dev diff --git a/apps/desktop/desktop_native/Cargo.lock b/apps/desktop/desktop_native/Cargo.lock index 82dbebb12df..148e56fcb0d 100644 --- a/apps/desktop/desktop_native/Cargo.lock +++ b/apps/desktop/desktop_native/Cargo.lock @@ -62,6 +62,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "anstyle" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" + [[package]] name = "anyhow" version = "1.0.93" @@ -147,9 +153,9 @@ dependencies = [ [[package]] name = "async-io" -version = "2.3.4" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "444b0228950ee6501b3568d3c93bf1176a1fdbc3b758dcd9475046d30f4dc7e8" +checksum = "43a2b323ccce0a1d90b449fd71f2a06ca7faa7c54c2751f06c9bd851fc061059" dependencies = [ "async-lock", "cfg-if", @@ -412,9 +418,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.8.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da" +checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" [[package]] name = "cbc" @@ -427,9 +433,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.1.34" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b9470d453346108f93a59222a9a1a5724db32d0a4727b7ab7ace4b4d822dc9" +checksum = "f34d93e62b03caf570cccc334cbc6c2fceca82f39211051345108adcba3eebdc" dependencies = [ "shlex", ] @@ -474,6 +480,32 @@ dependencies = [ "zeroize", ] +[[package]] +name = "clap" +version = "4.5.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69371e34337c4c984bbe322360c2547210bf632eb2814bbe78a6e87a2935bd2b" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.5.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e24c1b4099818523236a8ca881d2b45db98dadfb4625cf6608c12069fcbbde1" +dependencies = [ + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_lex" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afb84c814227b90d6895e01398aee0d8033c00e7466aca416fb6a8e0eb19d8a7" + [[package]] name = "clipboard-win" version = "5.4.0" @@ -517,6 +549,16 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation" version = "0.10.0" @@ -535,9 +577,9 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "cpufeatures" -version = "0.2.14" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" +checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3" dependencies = [ "libc", ] @@ -561,9 +603,9 @@ dependencies = [ [[package]] name = "ctor" -version = "0.2.8" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edb49164822f3ee45b17acd4a208cfc1251410cf0cad9a833234c9890774dd9f" +checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" dependencies = [ "quote", "syn", @@ -606,25 +648,26 @@ dependencies = [ [[package]] name = "cxx" -version = "1.0.129" +version = "1.0.133" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbdc8cca144dce1c4981b5c9ab748761619979e515c3d53b5df385c677d1d007" +checksum = "05e1ec88093d2abd9cf1b09ffd979136b8e922bf31cad966a8fe0d73233112ef" dependencies = [ "cc", + "cxxbridge-cmd", "cxxbridge-flags", "cxxbridge-macro", + "foldhash", "link-cplusplus", ] [[package]] name = "cxx-build" -version = "1.0.129" +version = "1.0.133" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5764c3142ab44fcf857101d12c0ddf09c34499900557c764f5ad0597159d1fc" +checksum = "9afa390d956ee7ccb41aeed7ed7856ab3ffb4fc587e7216be7e0f83e949b4e6c" dependencies = [ "cc", "codespan-reporting", - "once_cell", "proc-macro2", "quote", "scratch", @@ -632,19 +675,33 @@ dependencies = [ ] [[package]] -name = "cxxbridge-flags" -version = "1.0.129" +name = "cxxbridge-cmd" +version = "1.0.133" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d422aff542b4fa28c2ce8e5cc202d42dbf24702345c1fba3087b2d3f8a1b90ff" +checksum = "3c23bfff654d6227cbc83de8e059d2f8678ede5fc3a6c5a35d5c379983cc61e6" +dependencies = [ + "clap", + "codespan-reporting", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "cxxbridge-flags" +version = "1.0.133" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c01b36e22051bc6928a78583f1621abaaf7621561c2ada1b00f7878fbe2caa" [[package]] name = "cxxbridge-macro" -version = "1.0.129" +version = "1.0.133" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1719100f31492cd6adeeab9a0f46cdbc846e615fdb66d7b398aa46ec7fdd06f" +checksum = "f6e14013136fac689345d17b9a6df55977251f11d333c0a571e8d963b55e1f95" dependencies = [ "proc-macro2", "quote", + "rustversion", "syn", ] @@ -692,7 +749,8 @@ dependencies = [ "bitwarden-russh", "byteorder", "cbc", - "core-foundation", + "core-foundation 0.10.0", + "desktop_objc", "dirs", "ed25519", "futures", @@ -743,6 +801,18 @@ dependencies = [ "windows-registry", ] +[[package]] +name = "desktop_objc" +version = "0.0.0" +dependencies = [ + "anyhow", + "cc", + "core-foundation 0.9.4", + "glob", + "thiserror", + "tokio", +] + [[package]] name = "desktop_proxy" version = "0.0.0" @@ -874,12 +944,12 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.9" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -901,9 +971,9 @@ dependencies = [ [[package]] name = "event-listener-strategy" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1" +checksum = "3c3e4e0dd3673c1139bf041f3008816d9cf2946bbfac2945c09e523b8d7b05b2" dependencies = [ "event-listener", "pin-project-lite", @@ -911,9 +981,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" +checksum = "486f806e73c5707928240ddc295403b1b93c96a02038563881c4a2fd84b81ac4" [[package]] name = "fiat-crypto" @@ -933,6 +1003,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f81ec6369c545a7d40e4589b5597581fa1c441fe1cce96dd1de43159910a36a2" + [[package]] name = "futures" version = "0.3.31" @@ -983,9 +1059,9 @@ checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-lite" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f1fa2f9765705486b33fd2acf1577f8ec449c2ba1f318ae5447697b7c08d210" +checksum = "cef40d21ae2c515b51041df9ed313ed21e572df340ea58a922a0aefe7e8891a1" dependencies = [ "fastrand", "futures-core", @@ -1083,16 +1159,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] -name = "hashbrown" -version = "0.15.1" +name = "glob" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a9bfc1af68b1726ea47d3d5109de126281def866b33970e10fbab11b5dafab3" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] -name = "hermit-abi" -version = "0.3.9" +name = "hashbrown" +version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" [[package]] name = "hermit-abi" @@ -1138,9 +1214,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" +checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" dependencies = [ "equivalent", "hashbrown", @@ -1173,9 +1249,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.11" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" [[package]] name = "keytar" @@ -1215,9 +1291,9 @@ checksum = "18d287de67fe55fd7e1581fe933d965a5a9477b38e949cfa9f8574ef01506398" [[package]] name = "libloading" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" +checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" dependencies = [ "cfg-if", "windows-targets 0.52.6", @@ -1312,11 +1388,10 @@ dependencies = [ [[package]] name = "mio" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" dependencies = [ - "hermit-abi 0.3.9", "libc", "wasi", "windows-sys 0.52.0", @@ -1859,13 +1934,13 @@ checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" [[package]] name = "polling" -version = "3.7.3" +version = "3.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc2790cd301dec6cd3b7a025e4815cf825724a51c98dccfe6a3e55f05ffb6511" +checksum = "a604568c3202727d1507653cb121dbd627a58684eb09a820fd746bee38b4442f" dependencies = [ "cfg-if", "concurrent-queue", - "hermit-abi 0.4.0", + "hermit-abi", "pin-project-lite", "rustix", "tracing", @@ -1921,9 +1996,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.89" +version = "1.0.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" dependencies = [ "unicode-ident", ] @@ -2016,9 +2091,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.8" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", @@ -2079,18 +2154,18 @@ checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustc_version" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ "semver", ] [[package]] name = "rustix" -version = "0.38.37" +version = "0.38.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" +checksum = "d7f649912bc1495e167a6edee79151c84b1bad49748cb4f1f1167f459f6224f6" dependencies = [ "bitflags", "errno", @@ -2099,6 +2174,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rustversion" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248" + [[package]] name = "salsa20" version = "0.10.2" @@ -2144,7 +2225,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9d0283c0a4a22a0f1b0e4edca251aa20b92fc96eaa09b84bec052f9415e9d71" dependencies = [ "bitflags", - "core-foundation", + "core-foundation 0.10.0", "core-foundation-sys", "libc", "security-framework-sys", @@ -2168,18 +2249,18 @@ checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" [[package]] name = "serde" -version = "1.0.214" +version = "1.0.215" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f55c3193aca71c12ad7890f1785d2b73e1b9f63a0bbc353c08ef26fe03fc56b5" +checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.214" +version = "1.0.215" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766" +checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" dependencies = [ "proc-macro2", "quote", @@ -2272,9 +2353,9 @@ checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "socket2" -version = "0.5.7" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" dependencies = [ "libc", "windows-sys 0.52.0", @@ -2349,6 +2430,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "subtle" version = "2.6.1" @@ -2357,9 +2444,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.87" +version = "2.0.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" +checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" dependencies = [ "proc-macro2", "quote", @@ -2368,9 +2455,9 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.13.0" +version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0f2c9fc62d0beef6951ccffd757e241266a2c833136efbe35af6cd2567dca5b" +checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c" dependencies = [ "cfg-if", "fastrand", @@ -2410,9 +2497,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.36" +version = "0.3.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" dependencies = [ "deranged", "itoa", @@ -2433,9 +2520,9 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.18" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" dependencies = [ "num-conv", "time-core", @@ -2511,9 +2598,9 @@ dependencies = [ [[package]] name = "tracing" -version = "0.1.40" +version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ "pin-project-lite", "tracing-attributes", @@ -2522,9 +2609,9 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.27" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" dependencies = [ "proc-macro2", "quote", @@ -2533,9 +2620,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.32" +version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" dependencies = [ "once_cell", ] @@ -2572,9 +2659,9 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" [[package]] name = "unicode-segmentation" diff --git a/apps/desktop/desktop_native/core/Cargo.toml b/apps/desktop/desktop_native/core/Cargo.toml index 6e17cde2fa7..4ace1c13e08 100644 --- a/apps/desktop/desktop_native/core/Cargo.toml +++ b/apps/desktop/desktop_native/core/Cargo.toml @@ -23,7 +23,7 @@ sys = [ aes = "=0.8.4" anyhow = "=1.0.93" arboard = { version = "=3.4.1", default-features = false, features = [ - "wayland-data-control", + "wayland-data-control", ] } argon2 = { version = "=0.5.3", features = ["zeroize"] } async-stream = "=0.3.6" @@ -44,10 +44,10 @@ scopeguard = "=1.2.0" sha2 = "=0.10.8" ssh-encoding = "=0.2.0" ssh-key = { version = "=0.6.7", default-features = false, features = [ - "encryption", - "ed25519", - "rsa", - "getrandom", + "encryption", + "ed25519", + "rsa", + "getrandom", ] } bitwarden-russh = { git = "https://github.com/bitwarden/bitwarden-russh.git", rev = "b4e7f2fedbe3df8c35545feb000176d3e7b2bc32" } tokio = { version = "=1.41.1", features = ["io-util", "sync", "macros", "net"] } @@ -81,6 +81,7 @@ keytar = "=0.1.6" core-foundation = { version = "=0.10.0", optional = true } security-framework = { version = "=3.0.0", optional = true } security-framework-sys = { version = "=2.12.0", optional = true } +desktop_objc = { path = "../objc" } [target.'cfg(target_os = "linux")'.dependencies] oo7 = "=0.3.3" diff --git a/apps/desktop/desktop_native/core/src/autofill/macos.rs b/apps/desktop/desktop_native/core/src/autofill/macos.rs new file mode 100644 index 00000000000..08f333abe93 --- /dev/null +++ b/apps/desktop/desktop_native/core/src/autofill/macos.rs @@ -0,0 +1,5 @@ +use anyhow::Result; + +pub async fn run_command(value: String) -> Result { + desktop_objc::run_command(value).await +} diff --git a/apps/desktop/desktop_native/core/src/autofill/mod.rs b/apps/desktop/desktop_native/core/src/autofill/mod.rs new file mode 100644 index 00000000000..5997add240f --- /dev/null +++ b/apps/desktop/desktop_native/core/src/autofill/mod.rs @@ -0,0 +1,5 @@ +#[cfg_attr(target_os = "linux", path = "unix.rs")] +#[cfg_attr(target_os = "windows", path = "windows.rs")] +#[cfg_attr(target_os = "macos", path = "macos.rs")] +mod autofill; +pub use autofill::*; diff --git a/apps/desktop/desktop_native/core/src/autofill/unix.rs b/apps/desktop/desktop_native/core/src/autofill/unix.rs new file mode 100644 index 00000000000..d77130176a4 --- /dev/null +++ b/apps/desktop/desktop_native/core/src/autofill/unix.rs @@ -0,0 +1,5 @@ +use anyhow::Result; + +pub async fn run_command(value: String) -> Result { + todo!("Unix does not support autofill"); +} diff --git a/apps/desktop/desktop_native/core/src/autofill/windows.rs b/apps/desktop/desktop_native/core/src/autofill/windows.rs new file mode 100644 index 00000000000..2e442263c1b --- /dev/null +++ b/apps/desktop/desktop_native/core/src/autofill/windows.rs @@ -0,0 +1,5 @@ +use anyhow::Result; + +pub async fn run_command(value: String) -> Result { + todo!("Windows does not support autofill"); +} diff --git a/apps/desktop/desktop_native/core/src/lib.rs b/apps/desktop/desktop_native/core/src/lib.rs index f38e6ef97b4..b63c773209f 100644 --- a/apps/desktop/desktop_native/core/src/lib.rs +++ b/apps/desktop/desktop_native/core/src/lib.rs @@ -1,3 +1,4 @@ +pub mod autofill; #[cfg(feature = "sys")] pub mod biometric; #[cfg(feature = "sys")] @@ -8,9 +9,8 @@ pub mod ipc; #[cfg(feature = "sys")] pub mod password; #[cfg(feature = "sys")] -pub mod process_isolation; -#[cfg(feature = "sys")] pub mod powermonitor; #[cfg(feature = "sys")] - +pub mod process_isolation; +#[cfg(feature = "sys")] pub mod ssh_agent; diff --git a/apps/desktop/desktop_native/napi/index.d.ts b/apps/desktop/desktop_native/napi/index.d.ts index 9ceb30c4ff5..0eaba197919 100644 --- a/apps/desktop/desktop_native/napi/index.d.ts +++ b/apps/desktop/desktop_native/napi/index.d.ts @@ -122,6 +122,9 @@ export declare namespace ipc { send(message: string): number } } +export declare namespace autofill { + export function runCommand(value: string): Promise +} export declare namespace crypto { export function argon2(secret: Buffer, salt: Buffer, iterations: number, memory: number, parallelism: number): Promise } diff --git a/apps/desktop/desktop_native/napi/src/lib.rs b/apps/desktop/desktop_native/napi/src/lib.rs index f160b19ad53..5037108afd7 100644 --- a/apps/desktop/desktop_native/napi/src/lib.rs +++ b/apps/desktop/desktop_native/napi/src/lib.rs @@ -8,7 +8,8 @@ pub mod passwords { /// Fetch the stored password from the keychain. #[napi] pub async fn get_password(service: String, account: String) -> napi::Result { - desktop_core::password::get_password(&service, &account).await + desktop_core::password::get_password(&service, &account) + .await .map_err(|e| napi::Error::from_reason(e.to_string())) } @@ -19,21 +20,25 @@ pub mod passwords { account: String, password: String, ) -> napi::Result<()> { - desktop_core::password::set_password(&service, &account, &password).await + desktop_core::password::set_password(&service, &account, &password) + .await .map_err(|e| napi::Error::from_reason(e.to_string())) } /// Delete the stored password from the keychain. #[napi] pub async fn delete_password(service: String, account: String) -> napi::Result<()> { - desktop_core::password::delete_password(&service, &account).await + desktop_core::password::delete_password(&service, &account) + .await .map_err(|e| napi::Error::from_reason(e.to_string())) } // Checks if the os secure storage is available #[napi] pub async fn is_available() -> napi::Result { - desktop_core::password::is_available().await.map_err(|e| napi::Error::from_reason(e.to_string())) + desktop_core::password::is_available() + .await + .map_err(|e| napi::Error::from_reason(e.to_string())) } } @@ -244,13 +249,17 @@ pub mod sshagent { pub async fn serve( callback: ThreadsafeFunction<(String, bool), CalleeHandled>, ) -> napi::Result { - let (auth_request_tx, mut auth_request_rx) = tokio::sync::mpsc::channel::<(u32, (String, bool))>(32); - let (auth_response_tx, auth_response_rx) = tokio::sync::broadcast::channel::<(u32, bool)>(32); + let (auth_request_tx, mut auth_request_rx) = + tokio::sync::mpsc::channel::<(u32, (String, bool))>(32); + let (auth_response_tx, auth_response_rx) = + tokio::sync::broadcast::channel::<(u32, bool)>(32); let auth_response_tx_arc = Arc::new(Mutex::new(auth_response_tx)); tokio::spawn(async move { let _ = auth_response_rx; - while let Some((request_id, (cipher_uuid, is_list_request))) = auth_request_rx.recv().await { + while let Some((request_id, (cipher_uuid, is_list_request))) = + auth_request_rx.recv().await + { let cloned_request_id = request_id.clone(); let cloned_cipher_uuid = cipher_uuid.clone(); let cloned_response_tx_arc = auth_response_tx_arc.clone(); @@ -260,23 +269,33 @@ pub mod sshagent { let cipher_uuid = cloned_cipher_uuid; let auth_response_tx_arc = cloned_response_tx_arc; let callback = cloned_callback; - let promise_result: Result, napi::Error> = - callback.call_async(Ok((cipher_uuid, is_list_request))).await; + let promise_result: Result, napi::Error> = callback + .call_async(Ok((cipher_uuid, is_list_request))) + .await; match promise_result { Ok(promise_result) => match promise_result.await { Ok(result) => { - let _ = auth_response_tx_arc.lock().await.send((request_id, result)) + let _ = auth_response_tx_arc + .lock() + .await + .send((request_id, result)) .expect("should be able to send auth response to agent"); } Err(e) => { println!("[SSH Agent Native Module] calling UI callback promise was rejected: {}", e); - let _ = auth_response_tx_arc.lock().await.send((request_id, false)) + let _ = auth_response_tx_arc + .lock() + .await + .send((request_id, false)) .expect("should be able to send auth response to agent"); } }, Err(e) => { println!("[SSH Agent Native Module] calling UI callback could not create promise: {}", e); - let _ = auth_response_tx_arc.lock().await.send((request_id, false)) + let _ = auth_response_tx_arc + .lock() + .await + .send((request_id, false)) .expect("should be able to send auth response to agent"); } } @@ -343,7 +362,9 @@ pub mod sshagent { #[napi] pub fn clear_keys(agent_state: &mut SshAgentState) -> napi::Result<()> { let bitwarden_agent_state = &mut agent_state.state; - bitwarden_agent_state.clear_keys().map_err(|e| napi::Error::from_reason(e.to_string())) + bitwarden_agent_state + .clear_keys() + .map_err(|e| napi::Error::from_reason(e.to_string())) } #[napi] @@ -524,6 +545,16 @@ pub mod ipc { } } +#[napi] +pub mod autofill { + #[napi] + pub async fn run_command(value: String) -> napi::Result { + desktop_core::autofill::run_command(value) + .await + .map_err(|e| napi::Error::from_reason(e.to_string())) + } +} + #[napi] pub mod crypto { use napi::bindgen_prelude::Buffer; diff --git a/apps/desktop/desktop_native/objc/Cargo.toml b/apps/desktop/desktop_native/objc/Cargo.toml new file mode 100644 index 00000000000..4b883ce6fa7 --- /dev/null +++ b/apps/desktop/desktop_native/objc/Cargo.toml @@ -0,0 +1,21 @@ +[package] +edition = "2021" +license = "GPL-3.0" +name = "desktop_objc" +version = "0.0.0" +publish = false + +[features] +default = [] + +[dependencies] +anyhow = "=1.0.93" +thiserror = "=1.0.69" +tokio = "1.39.1" + +[target.'cfg(target_os = "macos")'.dependencies] +core-foundation = "=0.9.4" + +[build-dependencies] +cc = "1.0.104" +glob = "0.3.1" diff --git a/apps/desktop/desktop_native/objc/build.rs b/apps/desktop/desktop_native/objc/build.rs new file mode 100644 index 00000000000..57f3b626bf7 --- /dev/null +++ b/apps/desktop/desktop_native/objc/build.rs @@ -0,0 +1,22 @@ +use glob::glob; + +#[cfg(target_os = "macos")] +fn main() { + let mut builder = cc::Build::new(); + + // Auto compile all .m files in the src/native directory + for entry in glob("src/native/**/*.m").expect("Failed to read glob pattern") { + let path = entry.expect("Failed to read glob entry"); + builder.file(path.clone()); + println!("cargo::rerun-if-changed={}", path.display()); + } + + builder + .flag("-fobjc-arc") // Enable Auto Reference Counting (ARC) + .compile("autofill"); +} + +#[cfg(not(target_os = "macos"))] +fn main() { + // Crate is only supported on macOS +} diff --git a/apps/desktop/desktop_native/objc/src/lib.rs b/apps/desktop/desktop_native/objc/src/lib.rs new file mode 100644 index 00000000000..64475b7956d --- /dev/null +++ b/apps/desktop/desktop_native/objc/src/lib.rs @@ -0,0 +1,124 @@ +#![cfg(target_os = "macos")] + +use std::{ + ffi::{c_char, CStr, CString}, + os::raw::c_void, +}; + +use anyhow::{Context, Result}; + +#[repr(C)] +pub struct ObjCString { + value: *const c_char, + size: usize, +} + +#[repr(C)] +pub struct CommandContext { + tx: Option>, +} + +impl CommandContext { + pub fn new() -> (Self, tokio::sync::oneshot::Receiver) { + let (tx, rx) = tokio::sync::oneshot::channel::(); + + (CommandContext { tx: Some(tx) }, rx) + } + + pub fn send(&mut self, value: String) -> Result<()> { + let tx = self.tx.take().context( + "Failed to take Sender from CommandContext. Has this context already returned once?", + )?; + + tx.send(value).map_err(|_| { + anyhow::anyhow!("Failed to send ObjCString from CommandContext to Rust code") + })?; + + Ok(()) + } + + pub fn as_ptr(&mut self) -> *mut c_void { + self as *mut Self as *mut c_void + } +} + +impl TryFrom for String { + type Error = anyhow::Error; + + fn try_from(value: ObjCString) -> Result { + let c_str = unsafe { CStr::from_ptr(value.value) }; + let str = c_str + .to_str() + .context("Failed to convert ObjC output string to &str for use in Rust")?; + + Ok(str.to_owned()) + } +} + +impl Drop for ObjCString { + fn drop(&mut self) { + unsafe { + objc::freeObjCString(self); + } + } +} + +mod objc { + use std::os::raw::c_void; + + use super::*; + + extern "C" { + pub fn runCommand(context: *mut c_void, value: *const c_char); + pub fn freeObjCString(value: &ObjCString); + } + + /// This function is called from the ObjC code to return the output of the command + #[no_mangle] + pub extern "C" fn commandReturn(context: &mut CommandContext, value: ObjCString) -> bool { + let value: String = match value.try_into() { + Ok(value) => value, + Err(e) => { + println!( + "Error: Failed to convert ObjCString to Rust string during commandReturn: {}", + e + ); + + return false; + } + }; + + match context.send(value) { + Ok(_) => 0, + Err(e) => { + println!( + "Error: Failed to return ObjCString from ObjC code to Rust code: {}", + e + ); + + return false; + } + }; + + return true; + } +} + +pub async fn run_command(input: String) -> Result { + // Convert input to type that can be passed to ObjC code + let c_input = CString::new(input) + .context("Failed to convert Rust input string to a CString for use in call to ObjC code")?; + + let (mut context, rx) = CommandContext::new(); + + // Call ObjC code + unsafe { objc::runCommand(context.as_ptr(), c_input.as_ptr()) }; + + // Convert output from ObjC code to Rust string + let objc_output = rx.await?.try_into()?; + + // Convert output from ObjC code to Rust string + // let objc_output = output.try_into()?; + + Ok(objc_output) +} diff --git a/apps/desktop/desktop_native/objc/src/native/.clangd b/apps/desktop/desktop_native/objc/src/native/.clangd new file mode 100644 index 00000000000..5369bebcdc0 --- /dev/null +++ b/apps/desktop/desktop_native/objc/src/native/.clangd @@ -0,0 +1,2 @@ +CompileFlags: + Add: [-fobjc-arc] diff --git a/apps/desktop/desktop_native/objc/src/native/autofill/commands/status.h b/apps/desktop/desktop_native/objc/src/native/autofill/commands/status.h new file mode 100644 index 00000000000..e3e3c7969b6 --- /dev/null +++ b/apps/desktop/desktop_native/objc/src/native/autofill/commands/status.h @@ -0,0 +1,8 @@ +#ifndef STATUS_H +#define STATUS_H + +#import + +void status(void *context, NSDictionary *params); + +#endif diff --git a/apps/desktop/desktop_native/objc/src/native/autofill/commands/status.m b/apps/desktop/desktop_native/objc/src/native/autofill/commands/status.m new file mode 100644 index 00000000000..8811ffc6f0c --- /dev/null +++ b/apps/desktop/desktop_native/objc/src/native/autofill/commands/status.m @@ -0,0 +1,57 @@ +#import +#import +#import +#import "../../interop.h" +#import "status.h" + +void storeState(void (^callback)(ASCredentialIdentityStoreState*)) { + if (@available(macos 11, *)) { + ASCredentialIdentityStore *store = [ASCredentialIdentityStore sharedStore]; + [store getCredentialIdentityStoreStateWithCompletion:^(ASCredentialIdentityStoreState * _Nonnull state) { + callback(state); + }]; + } else { + callback(nil); + } +} + +BOOL fido2Supported() { + if (@available(macos 14, *)) { + return YES; + } else { + return NO; + } +} + +BOOL passwordSupported() { + if (@available(macos 11, *)) { + return YES; + } else { + return NO; + } +} + +void status(void* context, __attribute__((unused)) NSDictionary *params) { + storeState(^(ASCredentialIdentityStoreState *state) { + BOOL enabled = NO; + BOOL supportsIncremental = NO; + + if (state != nil) { + enabled = state.isEnabled; + supportsIncremental = state.supportsIncrementalUpdates; + } + + _return(context, + _success(@{ + @"support": @{ + @"fido2": @(fido2Supported()), + @"password": @(passwordSupported()), + @"incrementalUpdates": @(supportsIncremental), + }, + @"state": @{ + @"enabled": @(enabled), + } + }) + ); + }); +} diff --git a/apps/desktop/desktop_native/objc/src/native/autofill/commands/sync.h b/apps/desktop/desktop_native/objc/src/native/autofill/commands/sync.h new file mode 100644 index 00000000000..4eb39ff24d6 --- /dev/null +++ b/apps/desktop/desktop_native/objc/src/native/autofill/commands/sync.h @@ -0,0 +1,8 @@ +#ifndef SYNC_H +#define SYNC_H + +#import + +void runSync(void *context, NSDictionary *params); + +#endif diff --git a/apps/desktop/desktop_native/objc/src/native/autofill/commands/sync.m b/apps/desktop/desktop_native/objc/src/native/autofill/commands/sync.m new file mode 100644 index 00000000000..8b73635a7ca --- /dev/null +++ b/apps/desktop/desktop_native/objc/src/native/autofill/commands/sync.m @@ -0,0 +1,59 @@ +#import +#import +#import +#import +#import +#import +#import "../../utils.h" +#import "../../interop.h" +#import "sync.h" + +// 'run' is added to the name because it clashes with internal macOS function +void runSync(void* context, NSDictionary *params) { + NSArray *credentials = params[@"credentials"]; + + // Map credentials to ASPasswordCredential objects + NSMutableArray *mappedCredentials = [NSMutableArray arrayWithCapacity:credentials.count]; + for (NSDictionary *credential in credentials) { + NSString *type = credential[@"type"]; + + if ([type isEqualToString:@"password"]) { + NSString *cipherId = credential[@"cipherId"]; + NSString *uri = credential[@"uri"]; + NSString *username = credential[@"username"]; + + ASCredentialServiceIdentifier *serviceId = [[ASCredentialServiceIdentifier alloc] + initWithIdentifier:uri type:ASCredentialServiceIdentifierTypeURL]; + ASPasswordCredentialIdentity *credential = [[ASPasswordCredentialIdentity alloc] + initWithServiceIdentifier:serviceId user:username recordIdentifier:cipherId]; + + [mappedCredentials addObject:credential]; + } + + if ([type isEqualToString:@"fido2"]) { + NSString *cipherId = credential[@"cipherId"]; + NSString *rpId = credential[@"rpId"]; + NSString *userName = credential[@"userName"]; + NSData *credentialId = decodeBase64URL(credential[@"credentialId"]); + NSData *userHandle = decodeBase64URL(credential[@"userHandle"]); + + ASPasskeyCredentialIdentity *credential = [[ASPasskeyCredentialIdentity alloc] + initWithRelyingPartyIdentifier:rpId + userName:userName + credentialID:credentialId + userHandle:userHandle + recordIdentifier:cipherId]; + + [mappedCredentials addObject:credential]; + } + } + + [ASCredentialIdentityStore.sharedStore replaceCredentialIdentityEntries:mappedCredentials + completion:^(__attribute__((unused)) BOOL success, NSError * _Nullable error) { + if (error) { + return _return(context, _error_er(error)); + } + + _return(context, _success(@{@"added": @([mappedCredentials count])})); + }]; +} diff --git a/apps/desktop/desktop_native/objc/src/native/autofill/run_autofill_command.h b/apps/desktop/desktop_native/objc/src/native/autofill/run_autofill_command.h new file mode 100644 index 00000000000..fba5f626863 --- /dev/null +++ b/apps/desktop/desktop_native/objc/src/native/autofill/run_autofill_command.h @@ -0,0 +1,8 @@ +#ifndef RUN_AUTOFILL_COMMAND_H +#define RUN_AUTOFILL_COMMAND_H + +#import + +void runAutofillCommand(void* context, NSDictionary *input); + +#endif diff --git a/apps/desktop/desktop_native/objc/src/native/autofill/run_autofill_command.m b/apps/desktop/desktop_native/objc/src/native/autofill/run_autofill_command.m new file mode 100644 index 00000000000..46b188139bc --- /dev/null +++ b/apps/desktop/desktop_native/objc/src/native/autofill/run_autofill_command.m @@ -0,0 +1,20 @@ +#import +#import "commands/sync.h" +#import "commands/status.h" +#import "../interop.h" +#import "../utils.h" +#import "run_autofill_command.h" + +void runAutofillCommand(void* context, NSDictionary *input) { + NSString *command = input[@"command"]; + NSDictionary *params = input[@"params"]; + + if ([command isEqual:@"status"]) { + return status(context, params); + } else if ([command isEqual:@"sync"]) { + return runSync(context, params); + } + + _return(context, _error([NSString stringWithFormat:@"Unknown command: %@", command])); +} + diff --git a/apps/desktop/desktop_native/objc/src/native/interop.h b/apps/desktop/desktop_native/objc/src/native/interop.h new file mode 100644 index 00000000000..584fe547a9d --- /dev/null +++ b/apps/desktop/desktop_native/objc/src/native/interop.h @@ -0,0 +1,47 @@ +#ifndef INTEROP_H +#define INTEROP_H + +#import + +// Tips for developing Objective-C code: +// - Use the `NSLog` function to log messages to the system log +// - Example: +// NSLog(@"An example log: %@", someVariable); +// - Use the `@try` and `@catch` directives to catch exceptions + +#if !__has_feature(objc_arc) + // Auto Reference Counting makes memory management easier for Objective-C objects + // Regular C objects still need to be managed manually + #error ARC must be enabled! +#endif + +/// [Shared with Rust] +/// Simple struct to hold a C-string and its length +/// This is used to return strings created in Objective-C to Rust +/// so that Rust can free the memory when it's done with the string +struct ObjCString +{ + char *value; + size_t size; +}; + +/// [Defined in Rust] +/// External function callable from Objective-C to return a string to Rust +extern bool commandReturn(void *context, struct ObjCString output); + +/// [Callable from Rust] +/// Frees the memory allocated for an ObjCString +void freeObjCString(struct ObjCString *value); + +// --- Helper functions to convert between Objective-C and Rust types --- + +NSString *_success(NSDictionary *value); +NSString *_error(NSString *error); +NSString *_error_er(NSError *error); +NSString *_error_ex(NSException *error); +void _return(void *context, NSString *output); + +struct ObjCString nsStringToObjCString(NSString *string); +NSString *cStringToNSString(char *string); + +#endif diff --git a/apps/desktop/desktop_native/objc/src/native/interop.m b/apps/desktop/desktop_native/objc/src/native/interop.m new file mode 100644 index 00000000000..dc41eb52d76 --- /dev/null +++ b/apps/desktop/desktop_native/objc/src/native/interop.m @@ -0,0 +1,71 @@ +#import "interop.h" +#import "utils.h" + +/// [Callable from Rust] +/// Frees the memory allocated for an ObjCString +void freeObjCString(struct ObjCString *value) { + free(value->value); +} + +// --- Helper functions to convert between Objective-C and Rust types --- + +NSString *_success(NSDictionary *value) { + NSDictionary *wrapper = @{@"type": @"success", @"value": value}; + NSError *jsonError = nil; + NSString *toReturn = serializeJson(wrapper, jsonError); + + if (jsonError) { + // Manually format message since there seems to be an issue with the JSON serialization + return [NSString stringWithFormat:@"{\"type\": \"error\", \"error\": \"Error occurred while serializing error: %@\"}", jsonError]; + } + + return toReturn; +} + +NSString *_error(NSString *error) { + NSDictionary *errorDictionary = @{@"type": @"error", @"error": error}; + NSError *jsonError = nil; + NSString *toReturn = serializeJson(errorDictionary, jsonError); + + if (jsonError) { + // Manually format message since there seems to be an issue with the JSON serialization + return [NSString stringWithFormat:@"{\"type\": \"error\", \"error\": \"Error occurred while serializing error: %@\"}", jsonError]; + } + + return toReturn; +} + +NSString *_error_er(NSError *error) { + return _error([error localizedDescription]); +} + +NSString *_error_ex(NSException *error) { + return _error([NSString stringWithFormat:@"%@ (%@): %@", error.name, error.reason, [error callStackSymbols]]); +} + +void _return(void* context, NSString *output) { + if (!commandReturn(context, nsStringToObjCString(output))) { + NSLog(@"Error: Failed to return command output"); + // NOTE: This will most likely crash the application + @throw [NSException exceptionWithName:@"CommandReturnError" reason:@"Failed to return command output" userInfo:nil]; + } +} + +/// Converts an NSString to an ObjCString struct +struct ObjCString nsStringToObjCString(NSString* string) { + size_t size = [string lengthOfBytesUsingEncoding:NSUTF8StringEncoding] + 1; + char *value = malloc(size); + [string getCString:value maxLength:size encoding:NSUTF8StringEncoding]; + + struct ObjCString objCString; + objCString.value = value; + objCString.size = size; + + return objCString; +} + +/// Converts a C-string to an NSString +NSString* cStringToNSString(char* string) { + return [[NSString alloc] initWithUTF8String:string]; +} + diff --git a/apps/desktop/desktop_native/objc/src/native/run_command.m b/apps/desktop/desktop_native/objc/src/native/run_command.m new file mode 100644 index 00000000000..eb90a3340db --- /dev/null +++ b/apps/desktop/desktop_native/objc/src/native/run_command.m @@ -0,0 +1,39 @@ +#import +#import "autofill/run_autofill_command.h" +#import "interop.h" +#import "utils.h" + +void pickAndRunCommand(void* context, NSDictionary *input) { + NSString *namespace = input[@"namespace"]; + + if ([namespace isEqual:@"autofill"]) { + return runAutofillCommand(context, input); + } + + _return(context, _error([NSString stringWithFormat:@"Unknown namespace: %@", namespace])); +} + +/// [Callable from Rust] +/// Runs a command with the given input JSON +/// This function is called from Rust and is the entry point for running Objective-C code. +/// It takes a JSON string as input, deserializes it, runs the command, and serializes the output. +/// It also catches any exceptions that occur during the command execution. +void runCommand(void *context, char* inputJson) { + @autoreleasepool { + @try { + NSString *inputString = cStringToNSString(inputJson); + + NSError *error = nil; + NSDictionary *input = parseJson(inputString, error); + if (error) { + NSLog(@"Error occured while deserializing input params: %@", error); + return _return(context, _error([NSString stringWithFormat:@"Error occured while deserializing input params: %@", error])); + } + + pickAndRunCommand(context, input); + } @catch (NSException *e) { + NSLog(@"Error occurred while running Objective-C command: %@", e); + _return(context, _error([NSString stringWithFormat:@"Error occurred while running Objective-C command: %@", e])); + } + } +} diff --git a/apps/desktop/desktop_native/objc/src/native/utils.h b/apps/desktop/desktop_native/objc/src/native/utils.h new file mode 100644 index 00000000000..50fc991d15f --- /dev/null +++ b/apps/desktop/desktop_native/objc/src/native/utils.h @@ -0,0 +1,11 @@ +#ifndef UTILS_H +#define UTILS_H + +#import + +NSDictionary *parseJson(NSString *jsonString, NSError *error); +NSString *serializeJson(NSDictionary *dictionary, NSError *error); + +NSData *decodeBase64URL(NSString *base64URLString); + +#endif diff --git a/apps/desktop/desktop_native/objc/src/native/utils.m b/apps/desktop/desktop_native/objc/src/native/utils.m new file mode 100644 index 00000000000..040c723a8ac --- /dev/null +++ b/apps/desktop/desktop_native/objc/src/native/utils.m @@ -0,0 +1,28 @@ +#import "utils.h" + +NSDictionary *parseJson(NSString *jsonString, NSError *error) { + NSData *data = [jsonString dataUsingEncoding:NSUTF8StringEncoding]; + NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error]; + if (error) { + return nil; + } + return json; +} + +NSString *serializeJson(NSDictionary *dictionary, NSError *error) { + NSData *data = [NSJSONSerialization dataWithJSONObject:dictionary options:0 error:&error]; + if (error) { + return nil; + } + return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; +} + +NSData *decodeBase64URL(NSString *base64URLString) { + NSString *base64String = [base64URLString stringByReplacingOccurrencesOfString:@"-" withString:@"+"]; + base64String = [base64String stringByReplacingOccurrencesOfString:@"_" withString:@"/"]; + + NSData *nsdataFromBase64String = [[NSData alloc] + initWithBase64EncodedString:base64String options:0]; + + return nsdataFromBase64String; +} diff --git a/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift b/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift index d5c5cabeee4..4d03bf97e6c 100644 --- a/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift +++ b/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift @@ -19,17 +19,18 @@ class CredentialProviderViewController: ASCredentialProviderViewController { If using the credential would require showing custom UI for authenticating the user, cancel the request with error code ASExtensionError.userInteractionRequired. - override func provideCredentialWithoutUserInteraction(for credentialIdentity: ASPasswordCredentialIdentity) { - let databaseIsUnlocked = true - if (databaseIsUnlocked) { - let passwordCredential = ASPasswordCredential(user: "j_appleseed", password: "apple1234") - self.extensionContext.completeRequest(withSelectedCredential: passwordCredential, completionHandler: nil) - } else { - self.extensionContext.cancelRequest(withError: NSError(domain: ASExtensionErrorDomain, code:ASExtensionError.userInteractionRequired.rawValue)) - } - } - */ + */ + override func provideCredentialWithoutUserInteraction(for credentialIdentity: ASPasswordCredentialIdentity) { +// let databaseIsUnlocked = true +// if (databaseIsUnlocked) { + let passwordCredential = ASPasswordCredential(user: credentialIdentity.user, password: "example1234") + self.extensionContext.completeRequest(withSelectedCredential: passwordCredential, completionHandler: nil) +// } else { +// self.extensionContext.cancelRequest(withError: NSError(domain: ASExtensionErrorDomain, code:ASExtensionError.userInteractionRequired.rawValue)) +// } + } + /* Implement this method if provideCredentialWithoutUserInteraction(for:) can fail with ASExtensionError.userInteractionRequired. In this case, the system may present your extension's diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 54feb7df9e6..eab9a7d7119 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -48,6 +48,7 @@ "dist:mac": "npm run build && npm run pack:mac", "dist:mac:mas": "npm run build && npm run pack:mac:mas", "dist:mac:masdev": "npm run build && npm run pack:mac:masdev", + "dist:mac:masdev:with-extension": "npm run build && npm run pack:mac:masdev:with-extension", "dist:win": "npm run build && npm run pack:win", "dist:win:ci": "npm run build && npm run pack:win:ci", "publish:lin": "npm run build && npm run clean:dist && electron-builder --linux --x64 -p always", diff --git a/apps/desktop/scripts/build-macos-extension.js b/apps/desktop/scripts/build-macos-extension.js index 3aa43fb6785..4cb643c34d4 100644 --- a/apps/desktop/scripts/build-macos-extension.js +++ b/apps/desktop/scripts/build-macos-extension.js @@ -28,8 +28,9 @@ async function buildMacOs() { "-alltargets", "-configuration", "Release", - "-xcconfig", - paths.macOsConfig, + // Uncomment when signing is fixed + // "-xcconfig", + // paths.macOsConfig, ]); stdOutProc(proc); await new Promise((resolve, reject) => diff --git a/apps/desktop/src/app/services/init.service.ts b/apps/desktop/src/app/services/init.service.ts index 0068a03d685..de80f95593a 100644 --- a/apps/desktop/src/app/services/init.service.ts +++ b/apps/desktop/src/app/services/init.service.ts @@ -20,6 +20,7 @@ import { VaultTimeoutService } from "@bitwarden/common/services/vault-timeout/va import { UserId } from "@bitwarden/common/types/guid"; import { KeyService as KeyServiceAbstraction } from "@bitwarden/key-management"; +import { DesktopAutofillService } from "../../autofill/services/desktop-autofill.service"; import { I18nRendererService } from "../../platform/services/i18n.renderer.service"; import { SshAgentService } from "../../platform/services/ssh-agent.service"; import { VersionService } from "../../platform/services/version.service"; @@ -45,6 +46,7 @@ export class InitService { private accountService: AccountService, private versionService: VersionService, private sshAgentService: SshAgentService, + private autofillService: DesktopAutofillService, @Inject(DOCUMENT) private document: Document, ) {} @@ -82,6 +84,8 @@ export class InitService { const containerService = new ContainerService(this.keyService, this.encryptService); containerService.attachToGlobal(this.win); + + await this.autofillService.init(); }; } } diff --git a/apps/desktop/src/app/services/services.module.ts b/apps/desktop/src/app/services/services.module.ts index 040102d0395..8ca24c0040c 100644 --- a/apps/desktop/src/app/services/services.module.ts +++ b/apps/desktop/src/app/services/services.module.ts @@ -48,6 +48,7 @@ import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/s import { ClientType } from "@bitwarden/common/enums"; import { ProcessReloadServiceAbstraction } from "@bitwarden/common/key-management/abstractions/process-reload.service"; import { DefaultProcessReloadService } from "@bitwarden/common/key-management/services/default-process-reload.service"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; @@ -91,6 +92,7 @@ import { import { DesktopLoginApprovalComponentService } from "../../auth/login/desktop-login-approval-component.service"; import { DesktopLoginComponentService } from "../../auth/login/desktop-login-component.service"; import { DesktopAutofillSettingsService } from "../../autofill/services/desktop-autofill-settings.service"; +import { DesktopAutofillService } from "../../autofill/services/desktop-autofill.service"; import { ElectronBiometricsService } from "../../key-management/biometrics/electron-biometrics.service"; import { flagEnabled } from "../../platform/flags"; import { DesktopSettingsService } from "../../platform/services/desktop-settings.service"; @@ -301,6 +303,10 @@ const safeProviders: SafeProvider[] = [ provide: DesktopAutofillSettingsService, deps: [StateProvider], }), + safeProvider({ + provide: DesktopAutofillService, + deps: [LogService, CipherServiceAbstraction, ConfigService], + }), safeProvider({ provide: NativeMessagingManifestService, useClass: NativeMessagingManifestService, diff --git a/apps/desktop/src/autofill/preload.ts b/apps/desktop/src/autofill/preload.ts new file mode 100644 index 00000000000..9ce5e1319fd --- /dev/null +++ b/apps/desktop/src/autofill/preload.ts @@ -0,0 +1,9 @@ +import { ipcRenderer } from "electron"; + +import { Command } from "../platform/main/autofill/command"; +import { RunCommandParams, RunCommandResult } from "../platform/main/autofill/native-autofill.main"; + +export default { + runCommand: (params: RunCommandParams): Promise> => + ipcRenderer.invoke("autofill.runCommand", params), +}; diff --git a/apps/desktop/src/autofill/services/desktop-autofill.service.ts b/apps/desktop/src/autofill/services/desktop-autofill.service.ts new file mode 100644 index 00000000000..8c5dd597850 --- /dev/null +++ b/apps/desktop/src/autofill/services/desktop-autofill.service.ts @@ -0,0 +1,121 @@ +import { Injectable, OnDestroy } from "@angular/core"; +import { EMPTY, Subject, distinctUntilChanged, mergeMap, switchMap, takeUntil } from "rxjs"; + +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { UriMatchStrategy } from "@bitwarden/common/models/domain/domain-service"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { getCredentialsForAutofill } from "@bitwarden/common/platform/services/fido2/fido2-autofill-utils"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { CipherType } from "@bitwarden/common/vault/enums"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; + +import { NativeAutofillStatusCommand } from "../../platform/main/autofill/status.command"; +import { + NativeAutofillFido2Credential, + NativeAutofillPasswordCredential, + NativeAutofillSyncCommand, +} from "../../platform/main/autofill/sync.command"; + +@Injectable() +export class DesktopAutofillService implements OnDestroy { + private destroy$ = new Subject(); + + constructor( + private logService: LogService, + private cipherService: CipherService, + private configService: ConfigService, + ) {} + + async init() { + this.configService + .getFeatureFlag$(FeatureFlag.MacOsNativeCredentialSync) + .pipe( + distinctUntilChanged(), + switchMap((enabled) => { + if (!enabled) { + return EMPTY; + } + + return this.cipherService.cipherViews$; + }), + // TODO: This will unset all the autofill credentials on the OS + // when the account locks. We should instead explicilty clear the credentials + // when the user logs out. Maybe by subscribing to the encrypted ciphers observable instead. + mergeMap((cipherViewMap) => this.sync(Object.values(cipherViewMap ?? []))), + takeUntil(this.destroy$), + ) + .subscribe(); + } + + /** Give metadata about all available credentials in the users vault */ + async sync(cipherViews: CipherView[]) { + const status = await this.status(); + if (status.type === "error") { + return this.logService.error("Error getting autofill status", status.error); + } + + if (!status.value.state.enabled) { + // Autofill is disabled + return; + } + + let fido2Credentials: NativeAutofillFido2Credential[]; + let passwordCredentials: NativeAutofillPasswordCredential[]; + + if (status.value.support.password) { + passwordCredentials = cipherViews + .filter( + (cipher) => + cipher.type === CipherType.Login && + cipher.login.uris?.length > 0 && + cipher.login.uris.some((uri) => uri.match !== UriMatchStrategy.Never) && + cipher.login.uris.some((uri) => !Utils.isNullOrWhitespace(uri.uri)) && + !Utils.isNullOrWhitespace(cipher.login.username), + ) + .map((cipher) => ({ + type: "password", + cipherId: cipher.id, + uri: cipher.login.uris.find((uri) => uri.match !== UriMatchStrategy.Never).uri, + username: cipher.login.username, + })); + } + + if (status.value.support.fido2) { + fido2Credentials = (await getCredentialsForAutofill(cipherViews)).map((credential) => ({ + type: "fido2", + ...credential, + })); + } + + const syncResult = await ipc.autofill.runCommand({ + namespace: "autofill", + command: "sync", + params: { + credentials: [...fido2Credentials, ...passwordCredentials], + }, + }); + + if (syncResult.type === "error") { + return this.logService.error("Error syncing autofill credentials", syncResult.error); + } + + this.logService.debug(`Synced ${syncResult.value.added} autofill credentials`); + } + + /** Get autofill status from OS */ + private status() { + // TODO: Investigate why this type needs to be explicitly set + return ipc.autofill.runCommand({ + namespace: "autofill", + command: "status", + params: {}, + }); + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } +} diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index bbb5c23513e..5ae72150ff8 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -35,6 +35,7 @@ import { PowerMonitorMain } from "./main/power-monitor.main"; import { TrayMain } from "./main/tray.main"; import { UpdaterMain } from "./main/updater.main"; import { WindowMain } from "./main/window.main"; +import { NativeAutofillMain } from "./platform/main/autofill/native-autofill.main"; import { ClipboardMain } from "./platform/main/clipboard.main"; import { DesktopCredentialStorageListener } from "./platform/main/desktop-credential-storage-listener"; import { MainCryptoFunctionService } from "./platform/main/main-crypto-function.service"; @@ -72,6 +73,7 @@ export class Main { biometricsService: DesktopBiometricsService; nativeMessagingMain: NativeMessagingMain; clipboardMain: ClipboardMain; + nativeAutofillMain: NativeAutofillMain; desktopAutofillSettingsService: DesktopAutofillSettingsService; versionMain: VersionMain; sshAgentService: MainSshAgentService; @@ -256,6 +258,9 @@ export class Main { new EphemeralValueStorageService(); new SSOLocalhostCallbackService(this.environmentService, this.messagingService); + + this.nativeAutofillMain = new NativeAutofillMain(this.logService); + void this.nativeAutofillMain.init(); } bootstrap() { diff --git a/apps/desktop/src/platform/main/autofill/command.ts b/apps/desktop/src/platform/main/autofill/command.ts new file mode 100644 index 00000000000..a8b5548052b --- /dev/null +++ b/apps/desktop/src/platform/main/autofill/command.ts @@ -0,0 +1,23 @@ +import { NativeAutofillStatusCommand } from "./status.command"; +import { NativeAutofillSyncCommand } from "./sync.command"; + +export type CommandDefinition = { + namespace: string; + name: string; + input: Record; + output: Record; +}; + +export type CommandOutput = + | { + type: "error"; + error: string; + } + | { type: "success"; value: SuccessOutput }; + +export type IpcCommandInvoker = ( + params: C["input"], +) => Promise>; + +/** A list of all available commands */ +export type Command = NativeAutofillSyncCommand | NativeAutofillStatusCommand; diff --git a/apps/desktop/src/platform/main/autofill/native-autofill.main.ts b/apps/desktop/src/platform/main/autofill/native-autofill.main.ts new file mode 100644 index 00000000000..b4b7895e8ac --- /dev/null +++ b/apps/desktop/src/platform/main/autofill/native-autofill.main.ts @@ -0,0 +1,53 @@ +import { ipcMain } from "electron"; + +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { autofill } from "@bitwarden/desktop-napi"; + +import { CommandDefinition } from "./command"; + +export type RunCommandParams = { + namespace: C["namespace"]; + command: C["name"]; + params: C["input"]; +}; + +export type RunCommandResult = C["output"]; + +export class NativeAutofillMain { + constructor(private logService: LogService) {} + + async init() { + ipcMain.handle( + "autofill.runCommand", + ( + _event: any, + params: RunCommandParams, + ): Promise> => { + return this.runCommand(params); + }, + ); + } + + private async runCommand( + command: RunCommandParams, + ): Promise> { + try { + const result = await autofill.runCommand(JSON.stringify(command)); + const parsed = JSON.parse(result) as RunCommandResult; + + if (parsed.type === "error") { + this.logService.error(`Error running autofill command '${command.command}':`, parsed.error); + } + + return parsed; + } catch (e) { + this.logService.error(`Error running autofill command '${command.command}':`, e); + + if (e instanceof Error) { + return { type: "error", error: e.stack ?? String(e) } as RunCommandResult; + } + + return { type: "error", error: String(e) } as RunCommandResult; + } + } +} diff --git a/apps/desktop/src/platform/main/autofill/status.command.ts b/apps/desktop/src/platform/main/autofill/status.command.ts new file mode 100644 index 00000000000..b6c0943fa68 --- /dev/null +++ b/apps/desktop/src/platform/main/autofill/status.command.ts @@ -0,0 +1,20 @@ +import { CommandDefinition, CommandOutput } from "./command"; + +export interface NativeAutofillStatusCommand extends CommandDefinition { + name: "status"; + input: NativeAutofillStatusParams; + output: NativeAutofillStatusResult; +} + +export type NativeAutofillStatusParams = Record; + +export type NativeAutofillStatusResult = CommandOutput<{ + support: { + fido2: boolean; + password: boolean; + incrementalUpdates: boolean; + }; + state: { + enabled: boolean; + }; +}>; diff --git a/apps/desktop/src/platform/main/autofill/sync.command.ts b/apps/desktop/src/platform/main/autofill/sync.command.ts new file mode 100644 index 00000000000..dc3b12383ef --- /dev/null +++ b/apps/desktop/src/platform/main/autofill/sync.command.ts @@ -0,0 +1,37 @@ +import { CommandDefinition, CommandOutput } from "./command"; + +export interface NativeAutofillSyncCommand extends CommandDefinition { + name: "sync"; + input: NativeAutofillSyncParams; + output: NativeAutofillSyncResult; +} + +export type NativeAutofillSyncParams = { + credentials: NativeAutofillCredential[]; +}; + +export type NativeAutofillCredential = + | NativeAutofillFido2Credential + | NativeAutofillPasswordCredential; + +export type NativeAutofillFido2Credential = { + type: "fido2"; + cipherId: string; + rpId: string; + userName: string; + /** Should be Base64URL-encoded binary data */ + credentialId: string; + /** Should be Base64URL-encoded binary data */ + userHandle: string; +}; + +export type NativeAutofillPasswordCredential = { + type: "password"; + cipherId: string; + uri: string; + username: string; +}; + +export type NativeAutofillSyncResult = CommandOutput<{ + added: number; +}>; diff --git a/apps/desktop/src/preload.ts b/apps/desktop/src/preload.ts index 57b20490a4f..0fb2db37518 100644 --- a/apps/desktop/src/preload.ts +++ b/apps/desktop/src/preload.ts @@ -1,6 +1,7 @@ import { contextBridge } from "electron"; import auth from "./auth/preload"; +import autofill from "./autofill/preload"; import keyManagement from "./key-management/preload"; import platform from "./platform/preload"; @@ -17,6 +18,7 @@ import platform from "./platform/preload"; // Each team owns a subspace of the `ipc` global variable in the renderer. export const ipc = { auth, + autofill, platform, keyManagement, }; diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index 66d6b155e90..f79ebf8aa55 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -38,6 +38,7 @@ export enum FeatureFlag { NewDeviceVerificationTemporaryDismiss = "new-device-temporary-dismiss", NewDeviceVerificationPermanentDismiss = "new-device-permanent-dismiss", DisableFreeFamiliesSponsorship = "PM-12274-disable-free-families-sponsorship", + MacOsNativeCredentialSync = "macos-native-credential-sync", PM11360RemoveProviderExportPermission = "pm-11360-remove-provider-export-permission", } @@ -87,6 +88,7 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.NewDeviceVerificationTemporaryDismiss]: FALSE, [FeatureFlag.NewDeviceVerificationPermanentDismiss]: FALSE, [FeatureFlag.DisableFreeFamiliesSponsorship]: FALSE, + [FeatureFlag.MacOsNativeCredentialSync]: FALSE, [FeatureFlag.PM11360RemoveProviderExportPermission]: FALSE, } satisfies Record; diff --git a/libs/common/src/platform/services/fido2/fido2-autofill-utils.ts b/libs/common/src/platform/services/fido2/fido2-autofill-utils.ts new file mode 100644 index 00000000000..7ef705b95f9 --- /dev/null +++ b/libs/common/src/platform/services/fido2/fido2-autofill-utils.ts @@ -0,0 +1,26 @@ +// TODO: Add tests for this method + +import { CipherType } from "../../../vault/enums"; +import { CipherView } from "../../../vault/models/view/cipher.view"; +import { Fido2CredentialAutofillView } from "../../../vault/models/view/fido2-credential-autofill.view"; + +// TODO: Move into Fido2AuthenticatorService +export async function getCredentialsForAutofill( + ciphers: CipherView[], +): Promise { + return ciphers + .filter( + (cipher) => + !cipher.isDeleted && cipher.type === CipherType.Login && cipher.login.hasFido2Credentials, + ) + .map((cipher) => { + const credential = cipher.login.fido2Credentials[0]; + return { + cipherId: cipher.id, + credentialId: credential.credentialId, + rpId: credential.rpId, + userHandle: credential.userHandle, + userName: credential.userName, + }; + }); +} diff --git a/libs/common/src/vault/models/view/fido2-credential-autofill.view.ts b/libs/common/src/vault/models/view/fido2-credential-autofill.view.ts new file mode 100644 index 00000000000..b120f27b774 --- /dev/null +++ b/libs/common/src/vault/models/view/fido2-credential-autofill.view.ts @@ -0,0 +1,7 @@ +export class Fido2CredentialAutofillView { + cipherId: string; + credentialId: string; + rpId: string; + userHandle: string; + userName: string; +} From 7e2456224d0fab7b1c975e555523aa055d04bb7a Mon Sep 17 00:00:00 2001 From: Opeyemi Date: Fri, 6 Dec 2024 16:52:31 +0000 Subject: [PATCH 37/46] [BRE-470] - Update Renovate Conf for BRE team (#12274) --- .github/renovate.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/renovate.json b/.github/renovate.json index 9295ea5d61e..7f3e7464fe3 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -10,7 +10,7 @@ }, { "matchManagers": ["github-actions"], - "commitMessagePrefix": "[deps] DevOps:" + "commitMessagePrefix": "[deps] BRE:" }, { "matchManagers": ["cargo"], From 9fcc4f0543fce95c90b10616fff13e3a273d0457 Mon Sep 17 00:00:00 2001 From: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com> Date: Fri, 6 Dec 2024 12:43:17 -0500 Subject: [PATCH 38/46] PM-13659 - 2FA Timeout Log All the things (#12275) --- .../auth/components/two-factor.component.ts | 10 ++++++ .../abstractions/login-strategy.service.ts | 2 ++ .../login-strategy.service.ts | 36 ++++++++++++++++++- 3 files changed, 47 insertions(+), 1 deletion(-) diff --git a/libs/angular/src/auth/components/two-factor.component.ts b/libs/angular/src/auth/components/two-factor.component.ts index 33269e28e96..f484ccd1e8d 100644 --- a/libs/angular/src/auth/components/two-factor.component.ts +++ b/libs/angular/src/auth/components/two-factor.component.ts @@ -102,10 +102,20 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI super(environmentService, i18nService, platformUtilsService, toastService); this.webAuthnSupported = this.platformUtilsService.supportsWebAuthn(win); + this.logService.info( + "Subscribing to timeout on LoginStrategyService with service id: " + + this.loginStrategyService.id, + ); + // Add subscription to twoFactorTimeout$ and navigate to twoFactorTimeoutRoute if expired this.loginStrategyService.twoFactorTimeout$ .pipe(takeUntilDestroyed()) .subscribe(async (expired) => { + this.logService.info( + "Received emission from LoginStrategyService.twoFactorTimeout$ with service id: " + + this.loginStrategyService.id, + ); + if (!expired) { return; } diff --git a/libs/auth/src/common/abstractions/login-strategy.service.ts b/libs/auth/src/common/abstractions/login-strategy.service.ts index e86cd6b0b0d..89e8491b27e 100644 --- a/libs/auth/src/common/abstractions/login-strategy.service.ts +++ b/libs/auth/src/common/abstractions/login-strategy.service.ts @@ -14,6 +14,8 @@ import { } from "../models/domain/login-credentials"; export abstract class LoginStrategyServiceAbstraction { + id: string; + /** * The current strategy being used to authenticate. * Emits null if the session has timed out. diff --git a/libs/auth/src/common/services/login-strategies/login-strategy.service.ts b/libs/auth/src/common/services/login-strategies/login-strategy.service.ts index 1d5001f1f06..0c857cf7cc8 100644 --- a/libs/auth/src/common/services/login-strategies/login-strategy.service.ts +++ b/libs/auth/src/common/services/login-strategies/login-strategy.service.ts @@ -7,6 +7,7 @@ import { shareReplay, Subscription, BehaviorSubject, + tap, } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; @@ -31,6 +32,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; import { TaskSchedulerService, ScheduledTaskNames } from "@bitwarden/common/platform/scheduling"; import { GlobalState, GlobalStateProvider } from "@bitwarden/common/platform/state"; import { DeviceTrustServiceAbstraction } from "@bitwarden/common/src/auth/abstractions/device-trust.service.abstraction"; @@ -81,7 +83,36 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { private authRequestPushNotificationState: GlobalState; private twoFactorTimeoutSubject = new BehaviorSubject(false); - twoFactorTimeout$: Observable = this.twoFactorTimeoutSubject.asObservable(); + twoFactorTimeout$: Observable = this.twoFactorTimeoutSubject.asObservable().pipe( + // line 87 is the tap? + tap({ + next: (value) => { + this.logService.info( + `LoginStrategyService.twoFactorTimeout$ with service id: ${this.id} emmitted value: ${value}`, + ); + }, + error: (error: unknown) => { + this.logService.error( + `LoginStrategyService.twoFactorTimeout$ with service id: ${this.id} errored with error: ${JSON.stringify(error)}`, + ); + }, + finalize: () => { + this.logService.info( + `LoginStrategyService.twoFactorTimeout$ with service id: ${this.id} finalized`, + ); + }, + complete: () => { + this.logService.info( + `LoginStrategyService.twoFactorTimeout$ with service id: ${this.id} completed`, + ); + }, + subscribe: () => { + this.logService.info( + `LoginStrategyService.twoFactorTimeout$ with service id: ${this.id} subscribed`, + ); + }, + }), + ); private loginStrategy$: Observable< | UserApiLoginStrategy @@ -94,6 +125,8 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { currentAuthType$: Observable; + id: string = Utils.newGuid(); + constructor( protected accountService: AccountService, protected masterPasswordService: InternalMasterPasswordServiceAbstraction, @@ -131,6 +164,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { this.taskSchedulerService.registerTaskHandler( ScheduledTaskNames.loginStrategySessionTimeout, async () => { + this.logService.info("Timeout executing for LoginStrategyService with id: " + this.id); this.authnSessionTimeoutExecutor(async () => { this.twoFactorTimeoutSubject.next(true); try { From 597e3855619468ec662323c9eefcd527cbbaa22a Mon Sep 17 00:00:00 2001 From: rr-bw <102181210+rr-bw@users.noreply.github.com> Date: Fri, 6 Dec 2024 10:48:58 -0800 Subject: [PATCH 39/46] update height of iframe on web (#12265) --- apps/web/src/scss/plugins.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/scss/plugins.scss b/apps/web/src/scss/plugins.scss index f4aa428532c..5fbd32ac4ee 100644 --- a/apps/web/src/scss/plugins.scss +++ b/apps/web/src/scss/plugins.scss @@ -12,7 +12,7 @@ } #web-authn-frame { - height: 290px; + height: 315px; @include themify($themes) { background: themed("imgLoading") 0 0 no-repeat; } From d295825ff11943ba348415b3d90eeec8f8df8a64 Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Fri, 6 Dec 2024 11:29:35 -0800 Subject: [PATCH 40/46] [CL-511] - add variant to links within banners (#12090) * add variant to links within banners * add fix prop --- .../payment-method/organization-payment-method.component.html | 2 +- apps/web/src/app/billing/shared/payment-method.component.html | 2 +- .../individual-vault/vault-banners/vault-banners.component.html | 2 +- apps/web/src/app/vault/org-vault/vault.component.html | 2 +- .../src/app/secrets-manager/overview/overview.component.html | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.html b/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.html index 7a6e8558bae..e7bc9cac01e 100644 --- a/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.html +++ b/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.html @@ -9,7 +9,7 @@ {{ freeTrialData.message }} Date: Fri, 6 Dec 2024 13:42:32 -0600 Subject: [PATCH 41/46] revert: [PR-13659] remove 2FA timeout logging and fix attempts This reverts two previous commits: - PM-13659 - 2FA Timeout Log All the things (#12275) - Auth/PM-13659 - 2FA Timeout - Attempted Fix (#12263) --- .../service-container/service-container.ts | 7 --- .../auth/components/two-factor.component.ts | 10 ---- libs/angular/src/services/injection-tokens.ts | 6 +-- .../src/services/jslib-services.module.ts | 9 +--- .../abstractions/login-strategy.service.ts | 2 - .../login-strategy.service.ts | 53 +++---------------- 6 files changed, 9 insertions(+), 78 deletions(-) diff --git a/apps/cli/src/service-container/service-container.ts b/apps/cli/src/service-container/service-container.ts index ce944db78ff..21e8f9f2082 100644 --- a/apps/cli/src/service-container/service-container.ts +++ b/apps/cli/src/service-container/service-container.ts @@ -17,7 +17,6 @@ import { PinService, PinServiceAbstraction, UserDecryptionOptionsService, - Executor, } from "@bitwarden/auth/common"; import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service"; import { EventUploadService as EventUploadServiceAbstraction } from "@bitwarden/common/abstractions/event/event-upload.service"; @@ -615,11 +614,6 @@ export class ServiceContainer { this.configService, ); - // Execute any authn session timeout logic without any wrapping logic. - // An executor is required to ensure the logic is executed in an Angular context when it - // it is available. - const authnSessionTimeoutExecutor: Executor = (fn) => fn(); - this.loginStrategyService = new LoginStrategyService( this.accountService, this.masterPasswordService, @@ -646,7 +640,6 @@ export class ServiceContainer { this.vaultTimeoutSettingsService, this.kdfConfigService, this.taskSchedulerService, - authnSessionTimeoutExecutor, ); // FIXME: CLI does not support autofill diff --git a/libs/angular/src/auth/components/two-factor.component.ts b/libs/angular/src/auth/components/two-factor.component.ts index f484ccd1e8d..33269e28e96 100644 --- a/libs/angular/src/auth/components/two-factor.component.ts +++ b/libs/angular/src/auth/components/two-factor.component.ts @@ -102,20 +102,10 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI super(environmentService, i18nService, platformUtilsService, toastService); this.webAuthnSupported = this.platformUtilsService.supportsWebAuthn(win); - this.logService.info( - "Subscribing to timeout on LoginStrategyService with service id: " + - this.loginStrategyService.id, - ); - // Add subscription to twoFactorTimeout$ and navigate to twoFactorTimeoutRoute if expired this.loginStrategyService.twoFactorTimeout$ .pipe(takeUntilDestroyed()) .subscribe(async (expired) => { - this.logService.info( - "Received emission from LoginStrategyService.twoFactorTimeout$ with service id: " + - this.loginStrategyService.id, - ); - if (!expired) { return; } diff --git a/libs/angular/src/services/injection-tokens.ts b/libs/angular/src/services/injection-tokens.ts index 69a1ed3f613..86c5642a0c4 100644 --- a/libs/angular/src/services/injection-tokens.ts +++ b/libs/angular/src/services/injection-tokens.ts @@ -1,7 +1,7 @@ import { InjectionToken } from "@angular/core"; import { Observable, Subject } from "rxjs"; -import { Executor, LogoutReason } from "@bitwarden/auth/common"; +import { LogoutReason } from "@bitwarden/auth/common"; import { ClientType } from "@bitwarden/common/enums"; import { RegionConfig } from "@bitwarden/common/platform/abstractions/environment.service"; import { @@ -68,7 +68,3 @@ export const REFRESH_ACCESS_TOKEN_ERROR_CALLBACK = new SafeInjectionToken<() => export const ENV_ADDITIONAL_REGIONS = new SafeInjectionToken( "ENV_ADDITIONAL_REGIONS", ); - -export const AUTHN_SESSION_TIMEOUT_EXECUTOR = new SafeInjectionToken( - "AuthnSessionTimeoutExecutor", -); diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 8637e26a2b2..a43f1fa07a8 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -1,4 +1,4 @@ -import { ErrorHandler, LOCALE_ID, NgModule, NgZone } from "@angular/core"; +import { ErrorHandler, LOCALE_ID, NgModule } from "@angular/core"; import { Subject } from "rxjs"; import { @@ -319,7 +319,6 @@ import { CLIENT_TYPE, REFRESH_ACCESS_TOKEN_ERROR_CALLBACK, ENV_ADDITIONAL_REGIONS, - AUTHN_SESSION_TIMEOUT_EXECUTOR, } from "./injection-tokens"; import { ModalService } from "./modal.service"; @@ -412,11 +411,6 @@ const safeProviders: SafeProvider[] = [ TokenServiceAbstraction, ], }), - safeProvider({ - provide: AUTHN_SESSION_TIMEOUT_EXECUTOR, - useFactory: (ngZone: NgZone) => (fn: () => void) => ngZone.run(fn), - deps: [NgZone], - }), safeProvider({ provide: LoginStrategyServiceAbstraction, useClass: LoginStrategyService, @@ -446,7 +440,6 @@ const safeProviders: SafeProvider[] = [ VaultTimeoutSettingsServiceAbstraction, KdfConfigService, TaskSchedulerService, - AUTHN_SESSION_TIMEOUT_EXECUTOR, ], }), safeProvider({ diff --git a/libs/auth/src/common/abstractions/login-strategy.service.ts b/libs/auth/src/common/abstractions/login-strategy.service.ts index 89e8491b27e..e86cd6b0b0d 100644 --- a/libs/auth/src/common/abstractions/login-strategy.service.ts +++ b/libs/auth/src/common/abstractions/login-strategy.service.ts @@ -14,8 +14,6 @@ import { } from "../models/domain/login-credentials"; export abstract class LoginStrategyServiceAbstraction { - id: string; - /** * The current strategy being used to authenticate. * Emits null if the session has timed out. diff --git a/libs/auth/src/common/services/login-strategies/login-strategy.service.ts b/libs/auth/src/common/services/login-strategies/login-strategy.service.ts index 0c857cf7cc8..99e3c057e11 100644 --- a/libs/auth/src/common/services/login-strategies/login-strategy.service.ts +++ b/libs/auth/src/common/services/login-strategies/login-strategy.service.ts @@ -7,7 +7,6 @@ import { shareReplay, Subscription, BehaviorSubject, - tap, } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; @@ -32,7 +31,6 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; -import { Utils } from "@bitwarden/common/platform/misc/utils"; import { TaskSchedulerService, ScheduledTaskNames } from "@bitwarden/common/platform/scheduling"; import { GlobalState, GlobalStateProvider } from "@bitwarden/common/platform/state"; import { DeviceTrustServiceAbstraction } from "@bitwarden/common/src/auth/abstractions/device-trust.service.abstraction"; @@ -73,8 +71,6 @@ import { const sessionTimeoutLength = 5 * 60 * 1000; // 5 minutes -export type Executor = (fn: () => void) => void; - export class LoginStrategyService implements LoginStrategyServiceAbstraction { private sessionTimeoutSubscription: Subscription; private currentAuthnTypeState: GlobalState; @@ -83,36 +79,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { private authRequestPushNotificationState: GlobalState; private twoFactorTimeoutSubject = new BehaviorSubject(false); - twoFactorTimeout$: Observable = this.twoFactorTimeoutSubject.asObservable().pipe( - // line 87 is the tap? - tap({ - next: (value) => { - this.logService.info( - `LoginStrategyService.twoFactorTimeout$ with service id: ${this.id} emmitted value: ${value}`, - ); - }, - error: (error: unknown) => { - this.logService.error( - `LoginStrategyService.twoFactorTimeout$ with service id: ${this.id} errored with error: ${JSON.stringify(error)}`, - ); - }, - finalize: () => { - this.logService.info( - `LoginStrategyService.twoFactorTimeout$ with service id: ${this.id} finalized`, - ); - }, - complete: () => { - this.logService.info( - `LoginStrategyService.twoFactorTimeout$ with service id: ${this.id} completed`, - ); - }, - subscribe: () => { - this.logService.info( - `LoginStrategyService.twoFactorTimeout$ with service id: ${this.id} subscribed`, - ); - }, - }), - ); + twoFactorTimeout$: Observable = this.twoFactorTimeoutSubject.asObservable(); private loginStrategy$: Observable< | UserApiLoginStrategy @@ -125,8 +92,6 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { currentAuthType$: Observable; - id: string = Utils.newGuid(); - constructor( protected accountService: AccountService, protected masterPasswordService: InternalMasterPasswordServiceAbstraction, @@ -153,7 +118,6 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { protected vaultTimeoutSettingsService: VaultTimeoutSettingsService, protected kdfConfigService: KdfConfigService, protected taskSchedulerService: TaskSchedulerService, - private authnSessionTimeoutExecutor: Executor = (fn) => fn(), // Default to no-op ) { this.currentAuthnTypeState = this.stateProvider.get(CURRENT_LOGIN_STRATEGY_KEY); this.loginStrategyCacheState = this.stateProvider.get(CACHE_KEY); @@ -164,15 +128,12 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { this.taskSchedulerService.registerTaskHandler( ScheduledTaskNames.loginStrategySessionTimeout, async () => { - this.logService.info("Timeout executing for LoginStrategyService with id: " + this.id); - this.authnSessionTimeoutExecutor(async () => { - this.twoFactorTimeoutSubject.next(true); - try { - await this.clearCache(); - } catch (e) { - this.logService.error("Failed to clear cache during session timeout", e); - } - }); + this.twoFactorTimeoutSubject.next(true); + try { + await this.clearCache(); + } catch (e) { + this.logService.error("Failed to clear cache during session timeout", e); + } }, ); From 019a4d16c477ab67242ecc08cfaf65ca1579d144 Mon Sep 17 00:00:00 2001 From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Date: Fri, 6 Dec 2024 23:23:18 +0100 Subject: [PATCH 42/46] Resolve the Unauthorized issue on secrets manager (#12278) --- .../src/app/secrets-manager/overview/overview.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/overview/overview.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/overview/overview.component.ts index 3585f09faf6..534b1c4ea3c 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/overview/overview.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/overview/overview.component.ts @@ -140,7 +140,7 @@ export class OverviewComponent implements OnInit, OnDestroy { }); this.freeTrial$ = org$.pipe( - filter((org) => org.isOwner), + filter((org) => org.isOwner && org.canViewBillingHistory && org.canViewSubscription), switchMap((org) => combineLatest([ of(org), From a05162b373bca13ddad84765dec465b3b1734880 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 6 Dec 2024 19:09:05 -0600 Subject: [PATCH 43/46] [deps] AC: Update mini-css-extract-plugin to v2.9.2 (#11937) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 36b494c00c0..584a19adcd8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -155,7 +155,7 @@ "jest-mock-extended": "3.0.7", "jest-preset-angular": "14.1.1", "lint-staged": "15.2.8", - "mini-css-extract-plugin": "2.9.1", + "mini-css-extract-plugin": "2.9.2", "node-ipc": "9.2.1", "postcss": "8.4.47", "postcss-loader": "8.1.1", @@ -24579,9 +24579,9 @@ } }, "node_modules/mini-css-extract-plugin": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.1.tgz", - "integrity": "sha512-+Vyi+GCCOHnrJ2VPS+6aPoXN2k2jgUzDRhTFLjjTBn23qyXJXkjUWQgTL+mXpF5/A8ixLdCc6kWsoeOjKGejKQ==", + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.2.tgz", + "integrity": "sha512-GJuACcS//jtq4kCtd5ii/M0SZf7OZRH+BxdqXZHaJfb8TJiVl+NgQRPwiYt2EuqeSkNydn/7vP+bcE27C5mb9w==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 7d418b27d0c..4574e94d191 100644 --- a/package.json +++ b/package.json @@ -116,7 +116,7 @@ "jest-mock-extended": "3.0.7", "jest-preset-angular": "14.1.1", "lint-staged": "15.2.8", - "mini-css-extract-plugin": "2.9.1", + "mini-css-extract-plugin": "2.9.2", "node-ipc": "9.2.1", "postcss": "8.4.47", "postcss-loader": "8.1.1", From c6eb78d7fc08d49578e4c77396961b46b71da5f8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 6 Dec 2024 19:42:56 -0600 Subject: [PATCH 44/46] [deps] AC: Update postcss to v8.4.49 (#11938) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 10 +++++----- package.json | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 584a19adcd8..f1bc7e0c80f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -157,7 +157,7 @@ "lint-staged": "15.2.8", "mini-css-extract-plugin": "2.9.2", "node-ipc": "9.2.1", - "postcss": "8.4.47", + "postcss": "8.4.49", "postcss-loader": "8.1.1", "prettier": "3.3.3", "prettier-plugin-tailwindcss": "0.6.9", @@ -27224,9 +27224,9 @@ } }, "node_modules/postcss": { - "version": "8.4.47", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", - "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", + "version": "8.4.49", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", + "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", "dev": true, "funding": [ { @@ -27245,7 +27245,7 @@ "license": "MIT", "dependencies": { "nanoid": "^3.3.7", - "picocolors": "^1.1.0", + "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, "engines": { diff --git a/package.json b/package.json index 4574e94d191..1915aef2e96 100644 --- a/package.json +++ b/package.json @@ -118,7 +118,7 @@ "lint-staged": "15.2.8", "mini-css-extract-plugin": "2.9.2", "node-ipc": "9.2.1", - "postcss": "8.4.47", + "postcss": "8.4.49", "postcss-loader": "8.1.1", "prettier": "3.3.3", "prettier-plugin-tailwindcss": "0.6.9", From 1654ff9b7adc019c47626e2d2ffae26c0682574d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 6 Dec 2024 19:56:05 -0600 Subject: [PATCH 45/46] [deps] AC: Update sass-loader to v16.0.4 (#11309) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index f1bc7e0c80f..fa2484612ee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -165,7 +165,7 @@ "remark-gfm": "4.0.0", "rimraf": "6.0.1", "sass": "1.81.0", - "sass-loader": "16.0.1", + "sass-loader": "16.0.4", "storybook": "8.4.5", "style-loader": "3.3.4", "tailwindcss": "3.4.15", @@ -29135,9 +29135,9 @@ } }, "node_modules/sass-loader": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.1.tgz", - "integrity": "sha512-xACl1ToTsKnL9Ce5yYpRxrLj9QUDCnwZNhzpC7tKiFyA8zXsd3Ap+HGVnbCgkdQcm43E+i6oKAWBsvGA6ZoiMw==", + "version": "16.0.4", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.4.tgz", + "integrity": "sha512-LavLbgbBGUt3wCiYzhuLLu65+fWXaXLmq7YxivLhEqmiupCFZ5sKUAipK3do6V80YSU0jvSxNhEdT13IXNr3rg==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 1915aef2e96..e5a595a3f89 100644 --- a/package.json +++ b/package.json @@ -126,7 +126,7 @@ "remark-gfm": "4.0.0", "rimraf": "6.0.1", "sass": "1.81.0", - "sass-loader": "16.0.1", + "sass-loader": "16.0.4", "storybook": "8.4.5", "style-loader": "3.3.4", "tailwindcss": "3.4.15", From d5af932cef43066e91ca9ea145afe67303f27cf2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 6 Dec 2024 23:17:47 -0600 Subject: [PATCH 46/46] [deps] AC: Update style-loader to v4 (#10589) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 29 +++++++++++++++++++++++------ package.json | 2 +- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index fa2484612ee..1abea1dbcfc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -167,7 +167,7 @@ "sass": "1.81.0", "sass-loader": "16.0.4", "storybook": "8.4.5", - "style-loader": "3.3.4", + "style-loader": "4.0.0", "tailwindcss": "3.4.15", "ts-jest": "29.2.2", "ts-loader": "9.5.1", @@ -9098,6 +9098,23 @@ } } }, + "node_modules/@storybook/builder-webpack5/node_modules/style-loader": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.4.tgz", + "integrity": "sha512-0WqXzrsMTyb8yjZJHDqwmnwRJvhALK9LfRtRc6B4UTWe8AijYLZYZ9thuJTZc2VfQWINADW/j+LiJnfy2RoC1w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, "node_modules/@storybook/components": { "version": "8.4.5", "resolved": "https://registry.npmjs.org/@storybook/components/-/components-8.4.5.tgz", @@ -30494,20 +30511,20 @@ } }, "node_modules/style-loader": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.4.tgz", - "integrity": "sha512-0WqXzrsMTyb8yjZJHDqwmnwRJvhALK9LfRtRc6B4UTWe8AijYLZYZ9thuJTZc2VfQWINADW/j+LiJnfy2RoC1w==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-4.0.0.tgz", + "integrity": "sha512-1V4WqhhZZgjVAVJyt7TdDPZoPBPNHbekX4fWnCJL1yQukhCeZhJySUL+gL9y6sNdN95uEOS83Y55SqHcP7MzLA==", "dev": true, "license": "MIT", "engines": { - "node": ">= 12.13.0" + "node": ">= 18.12.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/webpack" }, "peerDependencies": { - "webpack": "^5.0.0" + "webpack": "^5.27.0" } }, "node_modules/sucrase": { diff --git a/package.json b/package.json index e5a595a3f89..68017a92362 100644 --- a/package.json +++ b/package.json @@ -128,7 +128,7 @@ "sass": "1.81.0", "sass-loader": "16.0.4", "storybook": "8.4.5", - "style-loader": "3.3.4", + "style-loader": "4.0.0", "tailwindcss": "3.4.15", "ts-jest": "29.2.2", "ts-loader": "9.5.1",