From 96f31aac3adcd3abcc917c523bcb7f671ab76ffa Mon Sep 17 00:00:00 2001 From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Date: Tue, 22 Jul 2025 15:58:17 +0100 Subject: [PATCH 01/21] [PM 18701]Optional payment modal after signup (#15384) * Implement the planservice * Add the pricing component and service * Add the change plan type service * resolve the unit test issues * Move the changeSubscriptionFrequency endpoint * Rename planservice to plancardservice * Remove unused and correct typos * Resolve the double asignment * resolve the unit test failing * Remove default payment setting to card * remove unnecessary check * Property initialPaymentMethod has no initializer * move the logic to service * Move estimate tax to pricing service * Refactor thr pricing summary component * Resolve the lint unit test error * Add changes for auto modal * Remove custom role for sm * Resolve the blank member page issue * Changes on the pricing display --- .../members/members.component.html | 5 + .../members/members.component.ts | 20 + .../organizations/members/members.module.ts | 2 + .../organization-payment-method.component.ts | 12 +- .../app/billing/services/plan-card.service.ts | 59 +++ .../services/pricing-summary.service.ts | 155 ++++++++ .../billing/shared/billing-shared.module.ts | 6 + .../shared/plan-card/plan-card.component.html | 45 +++ .../shared/plan-card/plan-card.component.ts | 68 ++++ .../pricing-summary.component.html | 259 +++++++++++++ .../pricing-summary.component.ts | 48 +++ .../trial-payment-dialog.component.html | 117 ++++++ .../trial-payment-dialog.component.ts | 365 ++++++++++++++++++ ...ganization-free-trial-warning.component.ts | 20 +- .../services/organization-warnings.service.ts | 34 +- apps/web/src/locales/en/messages.json | 29 +- ...ization-billing-api.service.abstraction.ts | 6 + .../request/change-plan-frequency.request.ts | 9 + .../organization-billing-api.service.ts | 14 + 19 files changed, 1264 insertions(+), 9 deletions(-) create mode 100644 apps/web/src/app/billing/services/plan-card.service.ts create mode 100644 apps/web/src/app/billing/services/pricing-summary.service.ts create mode 100644 apps/web/src/app/billing/shared/plan-card/plan-card.component.html create mode 100644 apps/web/src/app/billing/shared/plan-card/plan-card.component.ts create mode 100644 apps/web/src/app/billing/shared/pricing-summary/pricing-summary.component.html create mode 100644 apps/web/src/app/billing/shared/pricing-summary/pricing-summary.component.ts create mode 100644 apps/web/src/app/billing/shared/trial-payment-dialog/trial-payment-dialog.component.html create mode 100644 apps/web/src/app/billing/shared/trial-payment-dialog/trial-payment-dialog.component.ts create mode 100644 libs/common/src/billing/models/request/change-plan-frequency.request.ts diff --git a/apps/web/src/app/admin-console/organizations/members/members.component.html b/apps/web/src/app/admin-console/organizations/members/members.component.html index 962191021e8..49946806efc 100644 --- a/apps/web/src/app/admin-console/organizations/members/members.component.html +++ b/apps/web/src/app/admin-console/organizations/members/members.component.html @@ -1,3 +1,8 @@ + + protected deleteManagedMemberWarningService: DeleteManagedMemberWarningService, private configService: ConfigService, private organizationUserService: OrganizationUserService, + private organizationWarningsService: OrganizationWarningsService, ) { super( apiService, @@ -247,6 +250,13 @@ export class MembersComponent extends BaseMembersComponent this.showUserManagementControls$ = organization$.pipe( map((organization) => organization.canManageUsers), ); + organization$ + .pipe( + takeUntilDestroyed(), + tap((org) => (this.organization = org)), + switchMap((org) => this.organizationWarningsService.showInactiveSubscriptionDialog$(org)), + ) + .subscribe(); } async getUsers(): Promise { @@ -932,4 +942,14 @@ export class MembersComponent extends BaseMembersComponent .getCheckedUsers() .every((member) => member.managedByOrganization && validStatuses.includes(member.status)); } + + async navigateToPaymentMethod() { + const managePaymentDetailsOutsideCheckout = await this.configService.getFeatureFlag( + FeatureFlag.PM21881_ManagePaymentDetailsOutsideCheckout, + ); + const route = managePaymentDetailsOutsideCheckout ? "payment-details" : "payment-method"; + await this.router.navigate(["organizations", `${this.organization?.id}`, "billing", route], { + state: { launchPaymentModalAutomatically: true }, + }); + } } diff --git a/apps/web/src/app/admin-console/organizations/members/members.module.ts b/apps/web/src/app/admin-console/organizations/members/members.module.ts index 98431758d2f..5f626d44161 100644 --- a/apps/web/src/app/admin-console/organizations/members/members.module.ts +++ b/apps/web/src/app/admin-console/organizations/members/members.module.ts @@ -5,6 +5,7 @@ import { PasswordStrengthV2Component } from "@bitwarden/angular/tools/password-s import { PasswordCalloutComponent } from "@bitwarden/auth/angular"; import { ScrollLayoutDirective } from "@bitwarden/components"; +import { OrganizationFreeTrialWarningComponent } from "../../../billing/warnings/components"; import { LooseComponentsModule } from "../../../shared"; import { SharedOrganizationModule } from "../shared"; @@ -29,6 +30,7 @@ import { MembersComponent } from "./members.component"; ScrollingModule, PasswordStrengthV2Component, ScrollLayoutDirective, + OrganizationFreeTrialWarningComponent, ], declarations: [ BulkConfirmDialogComponent, 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 9b144fe59a7..aa7bf5e5d11 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 @@ -36,6 +36,10 @@ import { AdjustPaymentDialogComponent, AdjustPaymentDialogResultType, } from "../../shared/adjust-payment-dialog/adjust-payment-dialog.component"; +import { + TRIAL_PAYMENT_METHOD_DIALOG_RESULT_TYPE, + TrialPaymentDialogComponent, +} from "../../shared/trial-payment-dialog/trial-payment-dialog.component"; import { FreeTrial } from "../../types/free-trial"; @Component({ @@ -212,15 +216,15 @@ export class OrganizationPaymentMethodComponent implements OnDestroy { }; changePayment = async () => { - const dialogRef = AdjustPaymentDialogComponent.open(this.dialogService, { + const dialogRef = TrialPaymentDialogComponent.open(this.dialogService, { data: { - initialPaymentMethod: this.paymentSource?.type, organizationId: this.organizationId, - productTier: this.organization?.productTierType, + subscription: this.organizationSubscriptionResponse, + productTierType: this.organization?.productTierType, }, }); const result = await lastValueFrom(dialogRef.closed); - if (result === AdjustPaymentDialogResultType.Submitted) { + if (result === TRIAL_PAYMENT_METHOD_DIALOG_RESULT_TYPE.SUBMITTED) { this.location.replaceState(this.location.path(), "", {}); if (this.launchPaymentModalAutomatically && !this.organization.enabled) { await this.syncService.fullSync(true); diff --git a/apps/web/src/app/billing/services/plan-card.service.ts b/apps/web/src/app/billing/services/plan-card.service.ts new file mode 100644 index 00000000000..25974a428fd --- /dev/null +++ b/apps/web/src/app/billing/services/plan-card.service.ts @@ -0,0 +1,59 @@ +import { Injectable } from "@angular/core"; + +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.response"; +import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response"; + +@Injectable({ providedIn: "root" }) +export class PlanCardService { + constructor(private apiService: ApiService) {} + + async getCadenceCards( + currentPlan: PlanResponse, + subscription: OrganizationSubscriptionResponse, + isSecretsManagerTrial: boolean, + ) { + const plans = await this.apiService.getPlans(); + + const filteredPlans = plans.data.filter((plan) => !!plan.PasswordManager); + + const result = + filteredPlans?.filter( + (plan) => + plan.productTier === currentPlan.productTier && !plan.disabled && !plan.legacyYear, + ) || []; + + const planCards = result.map((plan) => { + let costPerMember = 0; + + if (plan.PasswordManager.basePrice) { + costPerMember = plan.isAnnual + ? plan.PasswordManager.basePrice / 12 + : plan.PasswordManager.basePrice; + } else if (!plan.PasswordManager.basePrice && plan.PasswordManager.hasAdditionalSeatsOption) { + const secretsManagerCost = subscription.useSecretsManager + ? plan.SecretsManager.seatPrice + : 0; + const passwordManagerCost = isSecretsManagerTrial ? 0 : plan.PasswordManager.seatPrice; + costPerMember = (secretsManagerCost + passwordManagerCost) / (plan.isAnnual ? 12 : 1); + } + + const percentOff = subscription.customerDiscount?.percentOff ?? 0; + + const discount = + (percentOff === 0 && plan.isAnnual) || isSecretsManagerTrial ? 20 : percentOff; + + return { + title: plan.isAnnual ? "Annually" : "Monthly", + costPerMember, + discount, + isDisabled: false, + isSelected: plan.isAnnual, + isAnnual: plan.isAnnual, + productTier: plan.productTier, + }; + }); + + return planCards.reverse(); + } +} diff --git a/apps/web/src/app/billing/services/pricing-summary.service.ts b/apps/web/src/app/billing/services/pricing-summary.service.ts new file mode 100644 index 00000000000..0b048b379d8 --- /dev/null +++ b/apps/web/src/app/billing/services/pricing-summary.service.ts @@ -0,0 +1,155 @@ +import { Injectable } from "@angular/core"; + +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { TaxServiceAbstraction } from "@bitwarden/common/billing/abstractions/tax.service.abstraction"; +import { PlanInterval, ProductTierType } from "@bitwarden/common/billing/enums"; +import { TaxInformation } from "@bitwarden/common/billing/models/domain/tax-information"; +import { PreviewOrganizationInvoiceRequest } from "@bitwarden/common/billing/models/request/preview-organization-invoice.request"; +import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.response"; +import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response"; + +import { PricingSummaryData } from "../shared/pricing-summary/pricing-summary.component"; + +@Injectable({ + providedIn: "root", +}) +export class PricingSummaryService { + private estimatedTax: number = 0; + + constructor(private taxService: TaxServiceAbstraction) {} + + async getPricingSummaryData( + plan: PlanResponse, + sub: OrganizationSubscriptionResponse, + organization: Organization, + selectedInterval: PlanInterval, + taxInformation: TaxInformation, + isSecretsManagerTrial: boolean, + ): Promise { + // Calculation helpers + const passwordManagerSeatTotal = + plan.PasswordManager?.hasAdditionalSeatsOption && !isSecretsManagerTrial + ? plan.PasswordManager.seatPrice * Math.abs(sub?.seats || 0) + : 0; + + const secretsManagerSeatTotal = plan.SecretsManager?.hasAdditionalSeatsOption + ? plan.SecretsManager.seatPrice * Math.abs(sub?.smSeats || 0) + : 0; + + const additionalServiceAccount = this.getAdditionalServiceAccount(plan, sub); + + const additionalStorageTotal = plan.PasswordManager?.hasAdditionalStorageOption + ? plan.PasswordManager.additionalStoragePricePerGb * + (sub?.maxStorageGb ? sub.maxStorageGb - 1 : 0) + : 0; + + const additionalStoragePriceMonthly = plan.PasswordManager?.additionalStoragePricePerGb || 0; + + const additionalServiceAccountTotal = + plan.SecretsManager?.hasAdditionalServiceAccountOption && additionalServiceAccount > 0 + ? plan.SecretsManager.additionalPricePerServiceAccount * additionalServiceAccount + : 0; + + let passwordManagerSubtotal = plan.PasswordManager?.basePrice || 0; + if (plan.PasswordManager?.hasAdditionalSeatsOption) { + passwordManagerSubtotal += passwordManagerSeatTotal; + } + if (plan.PasswordManager?.hasPremiumAccessOption) { + passwordManagerSubtotal += plan.PasswordManager.premiumAccessOptionPrice; + } + + const secretsManagerSubtotal = plan.SecretsManager + ? (plan.SecretsManager.basePrice || 0) + + secretsManagerSeatTotal + + additionalServiceAccountTotal + : 0; + + const totalAppliedDiscount = 0; + const discountPercentageFromSub = isSecretsManagerTrial + ? 0 + : (sub?.customerDiscount?.percentOff ?? 0); + const discountPercentage = 20; + const acceptingSponsorship = false; + const storageGb = sub?.maxStorageGb ? sub?.maxStorageGb - 1 : 0; + + this.estimatedTax = await this.getEstimatedTax(organization, plan, sub, taxInformation); + + const total = organization?.useSecretsManager + ? passwordManagerSubtotal + + additionalStorageTotal + + secretsManagerSubtotal + + this.estimatedTax + : passwordManagerSubtotal + additionalStorageTotal + this.estimatedTax; + + return { + selectedPlanInterval: selectedInterval === PlanInterval.Annually ? "year" : "month", + passwordManagerSeats: + plan.productTier === ProductTierType.Families ? plan.PasswordManager.baseSeats : sub?.seats, + passwordManagerSeatTotal, + secretsManagerSeatTotal, + additionalStorageTotal, + additionalStoragePriceMonthly, + additionalServiceAccountTotal, + totalAppliedDiscount, + secretsManagerSubtotal, + passwordManagerSubtotal, + total, + organization, + sub, + selectedPlan: plan, + selectedInterval, + discountPercentageFromSub, + discountPercentage, + acceptingSponsorship, + additionalServiceAccount, + storageGb, + isSecretsManagerTrial, + estimatedTax: this.estimatedTax, + }; + } + + async getEstimatedTax( + organization: Organization, + currentPlan: PlanResponse, + sub: OrganizationSubscriptionResponse, + taxInformation: TaxInformation, + ) { + if (!taxInformation || !taxInformation.country || !taxInformation.postalCode) { + return 0; + } + + const request: PreviewOrganizationInvoiceRequest = { + organizationId: organization.id, + passwordManager: { + additionalStorage: 0, + plan: currentPlan?.type, + seats: sub.seats, + }, + taxInformation: { + postalCode: taxInformation.postalCode, + country: taxInformation.country, + taxId: taxInformation.taxId, + }, + }; + + if (organization.useSecretsManager) { + request.secretsManager = { + seats: sub.smSeats ?? 0, + additionalMachineAccounts: + (sub.smServiceAccounts ?? 0) - (sub.plan.SecretsManager?.baseServiceAccount ?? 0), + }; + } + const invoiceResponse = await this.taxService.previewOrganizationInvoice(request); + return invoiceResponse.taxAmount; + } + + getAdditionalServiceAccount(plan: PlanResponse, sub: OrganizationSubscriptionResponse): number { + if (!plan || !plan.SecretsManager) { + return 0; + } + const baseServiceAccount = plan.SecretsManager?.baseServiceAccount || 0; + const usedServiceAccounts = sub?.smServiceAccounts || 0; + const additionalServiceAccounts = baseServiceAccount - usedServiceAccounts; + return additionalServiceAccounts <= 0 ? Math.abs(additionalServiceAccounts) : 0; + } +} diff --git a/apps/web/src/app/billing/shared/billing-shared.module.ts b/apps/web/src/app/billing/shared/billing-shared.module.ts index 9a69755b209..7322f047551 100644 --- a/apps/web/src/app/billing/shared/billing-shared.module.ts +++ b/apps/web/src/app/billing/shared/billing-shared.module.ts @@ -12,10 +12,13 @@ import { BillingHistoryComponent } from "./billing-history.component"; import { OffboardingSurveyComponent } from "./offboarding-survey.component"; import { PaymentComponent } from "./payment/payment.component"; import { PaymentMethodComponent } from "./payment-method.component"; +import { PlanCardComponent } from "./plan-card/plan-card.component"; +import { PricingSummaryComponent } from "./pricing-summary/pricing-summary.component"; import { IndividualSelfHostingLicenseUploaderComponent } from "./self-hosting-license-uploader/individual-self-hosting-license-uploader.component"; import { OrganizationSelfHostingLicenseUploaderComponent } from "./self-hosting-license-uploader/organization-self-hosting-license-uploader.component"; import { SecretsManagerSubscribeComponent } from "./sm-subscribe.component"; import { TaxInfoComponent } from "./tax-info.component"; +import { TrialPaymentDialogComponent } from "./trial-payment-dialog/trial-payment-dialog.component"; import { UpdateLicenseDialogComponent } from "./update-license-dialog.component"; import { UpdateLicenseComponent } from "./update-license.component"; import { VerifyBankAccountComponent } from "./verify-bank-account/verify-bank-account.component"; @@ -41,6 +44,9 @@ import { VerifyBankAccountComponent } from "./verify-bank-account/verify-bank-ac AdjustStorageDialogComponent, IndividualSelfHostingLicenseUploaderComponent, OrganizationSelfHostingLicenseUploaderComponent, + TrialPaymentDialogComponent, + PlanCardComponent, + PricingSummaryComponent, ], exports: [ SharedModule, diff --git a/apps/web/src/app/billing/shared/plan-card/plan-card.component.html b/apps/web/src/app/billing/shared/plan-card/plan-card.component.html new file mode 100644 index 00000000000..08fd3b435f6 --- /dev/null +++ b/apps/web/src/app/billing/shared/plan-card/plan-card.component.html @@ -0,0 +1,45 @@ +@let isFocused = plan().isSelected; +@let isRecommended = plan().isAnnual; + + +
+ @if (isRecommended) { +
+ {{ "recommended" | i18n }} +
+ } +
+

+ {{ plan().title }} + + + {{ "upgradeDiscount" | i18n: plan().discount }} +

+ + {{ plan().costPerMember | currency: "$" }} + /{{ "monthPerMember" | i18n }} + +
+
+
diff --git a/apps/web/src/app/billing/shared/plan-card/plan-card.component.ts b/apps/web/src/app/billing/shared/plan-card/plan-card.component.ts new file mode 100644 index 00000000000..9e3f03a5e7d --- /dev/null +++ b/apps/web/src/app/billing/shared/plan-card/plan-card.component.ts @@ -0,0 +1,68 @@ +import { Component, input, output } from "@angular/core"; + +import { ProductTierType } from "@bitwarden/common/billing/enums"; + +export interface PlanCard { + title: string; + costPerMember: number; + discount?: number; + isDisabled: boolean; + isAnnual: boolean; + isSelected: boolean; + productTier: ProductTierType; +} + +@Component({ + selector: "app-plan-card", + templateUrl: "./plan-card.component.html", + standalone: false, +}) +export class PlanCardComponent { + plan = input.required(); + productTiers = ProductTierType; + + cardClicked = output(); + + getPlanCardContainerClasses(): string[] { + const isSelected = this.plan().isSelected; + const isDisabled = this.plan().isDisabled; + if (isDisabled) { + return [ + "tw-cursor-not-allowed", + "tw-bg-secondary-100", + "tw-font-normal", + "tw-bg-blur", + "tw-text-muted", + "tw-block", + "tw-rounded", + ]; + } + + return isSelected + ? [ + "tw-cursor-pointer", + "tw-block", + "tw-rounded", + "tw-border", + "tw-border-solid", + "tw-border-primary-600", + "tw-border-2", + "tw-rounded-lg", + "hover:tw-border-primary-700", + "focus:tw-border-3", + "focus:tw-border-primary-700", + "focus:tw-rounded-lg", + ] + : [ + "tw-cursor-pointer", + "tw-block", + "tw-rounded", + "tw-border", + "tw-border-solid", + "tw-border-secondary-300", + "hover:tw-border-text-main", + "focus:tw-border-2", + "focus:tw-border-primary-700", + ]; + } +} diff --git a/apps/web/src/app/billing/shared/pricing-summary/pricing-summary.component.html b/apps/web/src/app/billing/shared/pricing-summary/pricing-summary.component.html new file mode 100644 index 00000000000..855b83bdb2d --- /dev/null +++ b/apps/web/src/app/billing/shared/pricing-summary/pricing-summary.component.html @@ -0,0 +1,259 @@ + +
+

+ {{ "total" | i18n }}: + {{ summaryData.total - summaryData.totalAppliedDiscount | currency: "USD" : "$" }} USD + / {{ summaryData.selectedPlanInterval | i18n }} + +

+
+ + + +
+ + + + + + + + + + + + + + +

{{ "passwordManager" | i18n }}

+ + + +

+ + + + {{ summaryData.passwordManagerSeats }} {{ "members" | i18n }} × + {{ + (summaryData.selectedPlan.isAnnual + ? summaryData.selectedPlan.PasswordManager.basePrice / 12 + : summaryData.selectedPlan.PasswordManager.basePrice + ) | currency: "$" + }} + /{{ summaryData.selectedPlanInterval | i18n }} + + + {{ "basePrice" | i18n }}: + {{ summaryData.selectedPlan.PasswordManager.basePrice | currency: "$" }} + {{ "monthAbbr" | i18n }} + + + + + + {{ + summaryData.selectedPlan.PasswordManager.basePrice | currency: "$" + }} + {{ "freeWithSponsorship" | i18n }} + + + {{ summaryData.selectedPlan.PasswordManager.basePrice | currency: "$" }} + + +

+
+ + + +

+ + {{ "additionalUsers" | i18n }}: + {{ summaryData.passwordManagerSeats || 0 }}  + {{ + "members" | i18n + }} + × + {{ summaryData.selectedPlan.PasswordManager.seatPrice | currency: "$" }} + /{{ summaryData.selectedPlanInterval | i18n }} + + + {{ summaryData.passwordManagerSeatTotal | currency: "$" }} + + + {{ "freeForOneYear" | i18n }} + +

+
+ + + +

+ + {{ summaryData.storageGb }} {{ "additionalStorageGbMessage" | i18n }} + × + {{ summaryData.additionalStoragePriceMonthly | currency: "$" }} + /{{ summaryData.selectedPlanInterval | i18n }} + + + + + {{ summaryData.additionalStorageTotal | currency: "$" }} + + + {{ + summaryData.storageGb * + summaryData.selectedPlan.PasswordManager.additionalStoragePricePerGb + | currency: "$" + }} + + + +

+
+
+
+ + + + +

{{ "secretsManager" | i18n }}

+ + + +

+ + + + {{ summaryData.sub?.smSeats }} {{ "members" | i18n }} × + {{ + (summaryData.selectedPlan.isAnnual + ? summaryData.selectedPlan.SecretsManager.basePrice / 12 + : summaryData.selectedPlan.SecretsManager.basePrice + ) | currency: "$" + }} + /{{ summaryData.selectedPlanInterval | i18n }} + + + {{ "basePrice" | i18n }}: + {{ summaryData.selectedPlan.SecretsManager.basePrice | currency: "$" }} + {{ "monthAbbr" | i18n }} + + + + + {{ summaryData.selectedPlan.SecretsManager.basePrice | currency: "$" }} + +

+
+ + + +

+ + {{ "additionalUsers" | i18n }}: + {{ summaryData.sub?.smSeats || 0 }}  + {{ + "members" | i18n + }} + × + {{ summaryData.selectedPlan.SecretsManager.seatPrice | currency: "$" }} + /{{ summaryData.selectedPlanInterval | i18n }} + + + {{ summaryData.secretsManagerSeatTotal | currency: "$" }} + +

+
+ + + +

+ + {{ summaryData.additionalServiceAccount }} + {{ "serviceAccounts" | i18n | lowercase }} + × + {{ + summaryData.selectedPlan?.SecretsManager?.additionalPricePerServiceAccount + | currency: "$" + }} + /{{ summaryData.selectedPlanInterval | i18n }} + + {{ summaryData.additionalServiceAccountTotal | currency: "$" }} +

+
+
+
+ + + +

+ + {{ + "providerDiscount" | i18n: this.summaryData.discountPercentageFromSub | lowercase + }} + + + {{ summaryData.totalAppliedDiscount | currency: "$" }} + +

+
+
+
+ + +
+ +

+ {{ "estimatedTax" | i18n }} + {{ summaryData.estimatedTax | currency: "USD" : "$" }} +

+
+
+ +
+ +

+ {{ "total" | i18n }} + + {{ summaryData.total - summaryData.totalAppliedDiscount | currency: "USD" : "$" }} + / {{ summaryData.selectedPlanInterval | i18n }} + +

+
+
+
+
diff --git a/apps/web/src/app/billing/shared/pricing-summary/pricing-summary.component.ts b/apps/web/src/app/billing/shared/pricing-summary/pricing-summary.component.ts new file mode 100644 index 00000000000..d4fdf35b743 --- /dev/null +++ b/apps/web/src/app/billing/shared/pricing-summary/pricing-summary.component.ts @@ -0,0 +1,48 @@ +import { Component, Input } from "@angular/core"; + +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { PlanInterval } from "@bitwarden/common/billing/enums"; +import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.response"; +import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response"; + +export interface PricingSummaryData { + selectedPlanInterval: string; + passwordManagerSeats: number; + passwordManagerSeatTotal: number; + secretsManagerSeatTotal: number; + additionalStorageTotal: number; + additionalStoragePriceMonthly: number; + additionalServiceAccountTotal: number; + totalAppliedDiscount: number; + secretsManagerSubtotal: number; + passwordManagerSubtotal: number; + total: number; + organization?: Organization; + sub?: OrganizationSubscriptionResponse; + selectedPlan?: PlanResponse; + selectedInterval?: PlanInterval; + discountPercentageFromSub?: number; + discountPercentage?: number; + acceptingSponsorship?: boolean; + additionalServiceAccount?: number; + totalOpened?: boolean; + storageGb?: number; + isSecretsManagerTrial?: boolean; + estimatedTax?: number; +} + +@Component({ + selector: "app-pricing-summary", + templateUrl: "./pricing-summary.component.html", + standalone: false, +}) +export class PricingSummaryComponent { + @Input() summaryData!: PricingSummaryData; + planIntervals = PlanInterval; + + toggleTotalOpened(): void { + if (this.summaryData) { + this.summaryData.totalOpened = !this.summaryData.totalOpened; + } + } +} diff --git a/apps/web/src/app/billing/shared/trial-payment-dialog/trial-payment-dialog.component.html b/apps/web/src/app/billing/shared/trial-payment-dialog/trial-payment-dialog.component.html new file mode 100644 index 00000000000..dbd2899c9e0 --- /dev/null +++ b/apps/web/src/app/billing/shared/trial-payment-dialog/trial-payment-dialog.component.html @@ -0,0 +1,117 @@ + + + {{ "subscribetoEnterprise" | i18n: currentPlanName }} + + +
+

{{ "subscribeEnterpriseSubtitle" | i18n: currentPlanName }}

+ + + +
    +
  • + + {{ "includeEnterprisePolicies" | i18n }} +
  • +
  • + + {{ "passwordLessSso" | i18n }} +
  • +
  • + + {{ "accountRecovery" | i18n }} +
  • +
  • + + {{ "customRoles" | i18n }} +
  • +
  • + + {{ "unlimitedSecretsAndProjects" | i18n }} +
  • +
+ +
    +
  • + + {{ "secureDataSharing" | i18n }} +
  • +
  • + + {{ "eventLogMonitoring" | i18n }} +
  • +
  • + + {{ "directoryIntegration" | i18n }} +
  • +
  • + + {{ "unlimitedSecretsAndProjects" | i18n }} +
  • +
+ +
    +
  • + + {{ "premiumAccounts" | i18n }} +
  • +
  • + + {{ "unlimitedSharing" | i18n }} +
  • +
  • + + {{ "createUnlimitedCollections" | i18n }} +
  • +
+
+ +
+
+

{{ "selectAPlan" | i18n }}

+
+ + +
+ @for (planCard of planCards(); track $index) { + + } +
+
+
+ + +

{{ "paymentMethod" | i18n }}

+ + + + + + +
+
+ + + + + +
diff --git a/apps/web/src/app/billing/shared/trial-payment-dialog/trial-payment-dialog.component.ts b/apps/web/src/app/billing/shared/trial-payment-dialog/trial-payment-dialog.component.ts new file mode 100644 index 00000000000..ca51ae80e1f --- /dev/null +++ b/apps/web/src/app/billing/shared/trial-payment-dialog/trial-payment-dialog.component.ts @@ -0,0 +1,365 @@ +import { Component, EventEmitter, Inject, OnInit, Output, signal, ViewChild } from "@angular/core"; +import { firstValueFrom, map } from "rxjs"; + +import { ManageTaxInformationComponent } from "@bitwarden/angular/billing/components"; +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; +import { + getOrganizationById, + 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 { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions"; +import { OrganizationBillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/organizations/organization-billing-api.service.abstraction"; +import { PaymentMethodType, PlanInterval, ProductTierType } from "@bitwarden/common/billing/enums"; +import { TaxInformation } from "@bitwarden/common/billing/models/domain"; +import { ChangePlanFrequencyRequest } from "@bitwarden/common/billing/models/request/change-plan-frequency.request"; +import { ExpandedTaxInfoUpdateRequest } from "@bitwarden/common/billing/models/request/expanded-tax-info-update.request"; +import { PreviewOrganizationInvoiceRequest } from "@bitwarden/common/billing/models/request/preview-organization-invoice.request"; +import { UpdatePaymentMethodRequest } from "@bitwarden/common/billing/models/request/update-payment-method.request"; +import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.response"; +import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response"; +import { ListResponse } from "@bitwarden/common/models/response/list.response"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { + DIALOG_DATA, + DialogConfig, + DialogRef, + DialogService, + ToastService, +} from "@bitwarden/components"; + +import { PlanCardService } from "../../services/plan-card.service"; +import { PaymentComponent } from "../payment/payment.component"; +import { PlanCard } from "../plan-card/plan-card.component"; +import { PricingSummaryData } from "../pricing-summary/pricing-summary.component"; + +import { PricingSummaryService } from "./../../services/pricing-summary.service"; + +type TrialPaymentDialogParams = { + organizationId: string; + subscription: OrganizationSubscriptionResponse; + productTierType: ProductTierType; + initialPaymentMethod?: PaymentMethodType; +}; + +export const TRIAL_PAYMENT_METHOD_DIALOG_RESULT_TYPE = { + CLOSED: "closed", + SUBMITTED: "submitted", +} as const; + +export type TrialPaymentDialogResultType = + (typeof TRIAL_PAYMENT_METHOD_DIALOG_RESULT_TYPE)[keyof typeof TRIAL_PAYMENT_METHOD_DIALOG_RESULT_TYPE]; + +interface OnSuccessArgs { + organizationId: string; +} + +@Component({ + selector: "app-trial-payment-dialog", + templateUrl: "./trial-payment-dialog.component.html", + standalone: false, +}) +export class TrialPaymentDialogComponent implements OnInit { + @ViewChild(PaymentComponent) paymentComponent!: PaymentComponent; + @ViewChild(ManageTaxInformationComponent) taxComponent!: ManageTaxInformationComponent; + + currentPlan!: PlanResponse; + currentPlanName!: string; + productTypes = ProductTierType; + organization!: Organization; + organizationId!: string; + sub!: OrganizationSubscriptionResponse; + selectedInterval: PlanInterval = PlanInterval.Annually; + + planCards = signal([]); + plans!: ListResponse; + + @Output() onSuccess = new EventEmitter(); + protected initialPaymentMethod: PaymentMethodType; + protected taxInformation!: TaxInformation; + protected readonly ResultType = TRIAL_PAYMENT_METHOD_DIALOG_RESULT_TYPE; + pricingSummaryData!: PricingSummaryData; + + constructor( + @Inject(DIALOG_DATA) private dialogParams: TrialPaymentDialogParams, + private dialogRef: DialogRef, + private organizationService: OrganizationService, + private i18nService: I18nService, + private organizationApiService: OrganizationApiServiceAbstraction, + private accountService: AccountService, + private planCardService: PlanCardService, + private pricingSummaryService: PricingSummaryService, + private apiService: ApiService, + private toastService: ToastService, + private billingApiService: BillingApiServiceAbstraction, + private organizationBillingApiServiceAbstraction: OrganizationBillingApiServiceAbstraction, + ) { + this.initialPaymentMethod = this.dialogParams.initialPaymentMethod ?? PaymentMethodType.Card; + } + + async ngOnInit(): Promise { + this.currentPlanName = this.resolvePlanName(this.dialogParams.productTierType); + this.sub = + this.dialogParams.subscription ?? + (await this.organizationApiService.getSubscription(this.dialogParams.organizationId)); + this.organizationId = this.dialogParams.organizationId; + this.currentPlan = this.sub?.plan; + const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(map((a) => a?.id))); + if (!userId) { + throw new Error("User ID is required"); + } + const organization = await firstValueFrom( + this.organizationService + .organizations$(userId) + .pipe(getOrganizationById(this.organizationId)), + ); + if (!organization) { + throw new Error("Organization not found"); + } + this.organization = organization; + + const planCards = await this.planCardService.getCadenceCards( + this.currentPlan, + this.sub, + this.isSecretsManagerTrial(), + ); + + this.planCards.set(planCards); + + if (!this.selectedInterval) { + this.selectedInterval = planCards.find((card) => card.isSelected)?.isAnnual + ? PlanInterval.Annually + : PlanInterval.Monthly; + } + + const taxInfo = await this.organizationApiService.getTaxInfo(this.organizationId); + this.taxInformation = TaxInformation.from(taxInfo); + + this.pricingSummaryData = await this.pricingSummaryService.getPricingSummaryData( + this.currentPlan, + this.sub, + this.organization, + this.selectedInterval, + this.taxInformation, + this.isSecretsManagerTrial(), + ); + + this.plans = await this.apiService.getPlans(); + } + + static open = ( + dialogService: DialogService, + dialogConfig: DialogConfig, + ) => dialogService.open(TrialPaymentDialogComponent, dialogConfig); + + async setSelected(planCard: PlanCard) { + this.selectedInterval = planCard.isAnnual ? PlanInterval.Annually : PlanInterval.Monthly; + + this.planCards.update((planCards) => { + return planCards.map((planCard) => { + if (planCard.isSelected) { + return { + ...planCard, + isSelected: false, + }; + } else { + return { + ...planCard, + isSelected: true, + }; + } + }); + }); + + await this.selectPlan(); + + this.pricingSummaryData = await this.pricingSummaryService.getPricingSummaryData( + this.currentPlan, + this.sub, + this.organization, + this.selectedInterval, + this.taxInformation, + this.isSecretsManagerTrial(), + ); + } + + protected async selectPlan() { + if ( + this.selectedInterval === PlanInterval.Monthly && + this.currentPlan.productTier == ProductTierType.Families + ) { + return; + } + + const filteredPlans = this.plans.data.filter( + (plan) => + plan.productTier === this.currentPlan.productTier && + plan.isAnnual === (this.selectedInterval === PlanInterval.Annually), + ); + if (filteredPlans.length > 0) { + this.currentPlan = filteredPlans[0]; + } + try { + await this.refreshSalesTax(); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + const translatedMessage = this.i18nService.t(errorMessage); + this.toastService.showToast({ + title: "", + variant: "error", + message: !translatedMessage || translatedMessage === "" ? errorMessage : translatedMessage, + }); + } + } + + protected get showTaxIdField(): boolean { + switch (this.currentPlan.productTier) { + case ProductTierType.Free: + case ProductTierType.Families: + return false; + default: + return true; + } + } + + private async refreshSalesTax(): Promise { + if ( + this.taxInformation === undefined || + !this.taxInformation.country || + !this.taxInformation.postalCode + ) { + return; + } + + const request: PreviewOrganizationInvoiceRequest = { + organizationId: this.organizationId, + passwordManager: { + additionalStorage: 0, + plan: this.currentPlan?.type, + seats: this.sub.seats, + }, + taxInformation: { + postalCode: this.taxInformation.postalCode, + country: this.taxInformation.country, + taxId: this.taxInformation.taxId, + }, + }; + + if (this.organization.useSecretsManager) { + request.secretsManager = { + seats: this.sub.smSeats ?? 0, + additionalMachineAccounts: + (this.sub.smServiceAccounts ?? 0) - + (this.sub.plan.SecretsManager?.baseServiceAccount ?? 0), + }; + } + + this.pricingSummaryData = await this.pricingSummaryService.getPricingSummaryData( + this.currentPlan, + this.sub, + this.organization, + this.selectedInterval, + this.taxInformation, + this.isSecretsManagerTrial(), + ); + } + + async taxInformationChanged(event: TaxInformation) { + this.taxInformation = event; + this.toggleBankAccount(); + await this.refreshSalesTax(); + } + + toggleBankAccount = () => { + this.paymentComponent.showBankAccount = this.taxInformation.country === "US"; + + if ( + !this.paymentComponent.showBankAccount && + this.paymentComponent.selected === PaymentMethodType.BankAccount + ) { + this.paymentComponent.select(PaymentMethodType.Card); + } + }; + + isSecretsManagerTrial(): boolean { + return ( + this.sub?.subscription?.items?.some((item) => + this.sub?.customerDiscount?.appliesTo?.includes(item.productId), + ) ?? false + ); + } + + async onSubscribe(): Promise { + if (!this.taxComponent.validate()) { + this.taxComponent.markAllAsTouched(); + } + try { + await this.updateOrganizationPaymentMethod( + this.organizationId, + this.paymentComponent, + this.taxInformation, + ); + + if (this.currentPlan.type !== this.sub.planType) { + const changePlanRequest = new ChangePlanFrequencyRequest(); + changePlanRequest.newPlanType = this.currentPlan.type; + await this.organizationBillingApiServiceAbstraction.changeSubscriptionFrequency( + this.organizationId, + changePlanRequest, + ); + } + + this.toastService.showToast({ + variant: "success", + title: undefined, + message: this.i18nService.t("updatedPaymentMethod"), + }); + + this.onSuccess.emit({ organizationId: this.organizationId }); + this.dialogRef.close(TRIAL_PAYMENT_METHOD_DIALOG_RESULT_TYPE.SUBMITTED); + } catch (error) { + const msg = + typeof error === "object" && error !== null && "message" in error + ? (error as { message: string }).message + : String(error); + this.toastService.showToast({ + variant: "error", + title: undefined, + message: this.i18nService.t(msg) || msg, + }); + } + } + + private async updateOrganizationPaymentMethod( + organizationId: string, + paymentComponent: PaymentComponent, + taxInformation: TaxInformation, + ): Promise { + const paymentSource = await paymentComponent.tokenize(); + + const request = new UpdatePaymentMethodRequest(); + request.paymentSource = paymentSource; + request.taxInformation = ExpandedTaxInfoUpdateRequest.From(taxInformation); + + await this.billingApiService.updateOrganizationPaymentMethod(organizationId, request); + } + + resolvePlanName(productTier: ProductTierType): string { + switch (productTier) { + case ProductTierType.Enterprise: + return this.i18nService.t("planNameEnterprise"); + case ProductTierType.Free: + return this.i18nService.t("planNameFree"); + case ProductTierType.Families: + return this.i18nService.t("planNameFamilies"); + case ProductTierType.Teams: + return this.i18nService.t("planNameTeams"); + case ProductTierType.TeamsStarter: + return this.i18nService.t("planNameTeamsStarter"); + default: + return this.i18nService.t("planNameFree"); + } + } +} diff --git a/apps/web/src/app/billing/warnings/components/organization-free-trial-warning.component.ts b/apps/web/src/app/billing/warnings/components/organization-free-trial-warning.component.ts index 074358537b6..a7ce53c9998 100644 --- a/apps/web/src/app/billing/warnings/components/organization-free-trial-warning.component.ts +++ b/apps/web/src/app/billing/warnings/components/organization-free-trial-warning.component.ts @@ -1,8 +1,10 @@ import { AsyncPipe } from "@angular/common"; -import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core"; -import { Observable } from "rxjs"; +import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core"; +import { Observable, Subject } from "rxjs"; +import { takeUntil } from "rxjs/operators"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { OrganizationId } from "@bitwarden/common/types/guid"; import { AnchorLinkDirective, BannerComponent } from "@bitwarden/components"; import { I18nPipe } from "@bitwarden/ui-common"; @@ -37,16 +39,28 @@ import { OrganizationFreeTrialWarning } from "../types"; `, imports: [AnchorLinkDirective, AsyncPipe, BannerComponent, I18nPipe], }) -export class OrganizationFreeTrialWarningComponent implements OnInit { +export class OrganizationFreeTrialWarningComponent implements OnInit, OnDestroy { @Input({ required: true }) organization!: Organization; @Output() clicked = new EventEmitter(); warning$!: Observable; + private destroy$ = new Subject(); constructor(private organizationWarningsService: OrganizationWarningsService) {} ngOnInit() { this.warning$ = this.organizationWarningsService.getFreeTrialWarning$(this.organization); + this.organizationWarningsService + .refreshWarningsForOrganization$(this.organization.id as OrganizationId) + .pipe(takeUntil(this.destroy$)) + .subscribe(() => { + this.refresh(); + }); + } + + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); } refresh = () => { diff --git a/apps/web/src/app/billing/warnings/services/organization-warnings.service.ts b/apps/web/src/app/billing/warnings/services/organization-warnings.service.ts index fa53992afe0..78c17a5d384 100644 --- a/apps/web/src/app/billing/warnings/services/organization-warnings.service.ts +++ b/apps/web/src/app/billing/warnings/services/organization-warnings.service.ts @@ -1,6 +1,7 @@ +import { Location } from "@angular/common"; import { Injectable } from "@angular/core"; import { Router } from "@angular/router"; -import { filter, from, lastValueFrom, map, Observable, switchMap, takeWhile } from "rxjs"; +import { filter, from, lastValueFrom, map, Observable, Subject, switchMap, takeWhile } from "rxjs"; import { take } from "rxjs/operators"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; @@ -10,10 +11,15 @@ import { OrganizationWarningsResponse } from "@bitwarden/common/billing/models/r import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { SyncService } from "@bitwarden/common/platform/sync"; import { OrganizationId } from "@bitwarden/common/types/guid"; import { DialogService } from "@bitwarden/components"; import { openChangePlanDialog } from "../../organizations/change-plan-dialog.component"; +import { + TRIAL_PAYMENT_METHOD_DIALOG_RESULT_TYPE, + TrialPaymentDialogComponent, +} from "../../shared/trial-payment-dialog/trial-payment-dialog.component"; import { OrganizationFreeTrialWarning, OrganizationResellerRenewalWarning } from "../types"; const format = (date: Date) => @@ -26,6 +32,7 @@ const format = (date: Date) => @Injectable({ providedIn: "root" }) export class OrganizationWarningsService { private cache$ = new Map>(); + private refreshWarnings$ = new Subject(); constructor( private configService: ConfigService, @@ -34,6 +41,8 @@ export class OrganizationWarningsService { private organizationApiService: OrganizationApiServiceAbstraction, private organizationBillingApiService: OrganizationBillingApiServiceAbstraction, private router: Router, + private location: Location, + protected syncService: SyncService, ) {} getFreeTrialWarning$ = ( @@ -174,10 +183,33 @@ export class OrganizationWarningsService { }); break; } + case "add_payment_method_optional_trial": { + const organizationSubscriptionResponse = + await this.organizationApiService.getSubscription(organization.id); + + const dialogRef = TrialPaymentDialogComponent.open(this.dialogService, { + data: { + organizationId: organization.id, + subscription: organizationSubscriptionResponse, + productTierType: organization?.productTierType, + }, + }); + const result = await lastValueFrom(dialogRef.closed); + if (result === TRIAL_PAYMENT_METHOD_DIALOG_RESULT_TYPE.SUBMITTED) { + this.refreshWarnings$.next(organization.id as OrganizationId); + } + } } }), ); + refreshWarningsForOrganization$(organizationId: OrganizationId): Observable { + return this.refreshWarnings$.pipe( + filter((id) => id === organizationId), + map((): void => void 0), + ); + } + private getResponse$ = ( organization: Organization, bypassCache: boolean = false, diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index d5ded3c75ea..9c9ecc79721 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -568,6 +568,9 @@ "cancel": { "message": "Cancel" }, + "later": { + "message": "Later" + }, "canceled": { "message": "Canceled" }, @@ -4630,6 +4633,9 @@ "receiveMarketingEmailsV2": { "message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." }, + "subscribe": { + "message": "Subscribe" + }, "unsubscribe": { "message": "Unsubscribe" }, @@ -10900,5 +10906,26 @@ "example": "12-3456789" } } + }, + "subscribetoEnterprise": { + "message": "Subscribe to $PLAN$", + "placeholders": { + "plan": { + "content": "$1", + "example": "Teams" + } + } + }, + "subscribeEnterpriseSubtitle": { + "message": "Your 7-day $PLAN$ trial starts today. Add a payment method now to continue using these features after your trial ends: ", + "placeholders": { + "plan": { + "content": "$1", + "example": "Teams" + } + } + }, + "unlimitedSecretsAndProjects": { + "message": "Unlimited secrets and projects" } -} +} \ No newline at end of file diff --git a/libs/common/src/billing/abstractions/organizations/organization-billing-api.service.abstraction.ts b/libs/common/src/billing/abstractions/organizations/organization-billing-api.service.abstraction.ts index 4975da0d7d2..29301e626b9 100644 --- a/libs/common/src/billing/abstractions/organizations/organization-billing-api.service.abstraction.ts +++ b/libs/common/src/billing/abstractions/organizations/organization-billing-api.service.abstraction.ts @@ -1,3 +1,4 @@ +import { ChangePlanFrequencyRequest } from "@bitwarden/common/billing/models/request/change-plan-frequency.request"; import { OrganizationWarningsResponse } from "@bitwarden/common/billing/models/response/organization-warnings.response"; import { @@ -28,4 +29,9 @@ export abstract class OrganizationBillingApiServiceAbstraction { organizationKey: string; }, ) => Promise; + + abstract changeSubscriptionFrequency: ( + organizationId: string, + request: ChangePlanFrequencyRequest, + ) => Promise; } diff --git a/libs/common/src/billing/models/request/change-plan-frequency.request.ts b/libs/common/src/billing/models/request/change-plan-frequency.request.ts new file mode 100644 index 00000000000..70b77181663 --- /dev/null +++ b/libs/common/src/billing/models/request/change-plan-frequency.request.ts @@ -0,0 +1,9 @@ +import { PlanType } from "../../enums"; + +export class ChangePlanFrequencyRequest { + newPlanType: PlanType; + + constructor(newPlanType?: PlanType) { + this.newPlanType = newPlanType!; + } +} diff --git a/libs/common/src/billing/services/organization/organization-billing-api.service.ts b/libs/common/src/billing/services/organization/organization-billing-api.service.ts index 1189316a487..e9456f61026 100644 --- a/libs/common/src/billing/services/organization/organization-billing-api.service.ts +++ b/libs/common/src/billing/services/organization/organization-billing-api.service.ts @@ -1,3 +1,4 @@ +import { ChangePlanFrequencyRequest } from "@bitwarden/common/billing/models/request/change-plan-frequency.request"; import { OrganizationWarningsResponse } from "@bitwarden/common/billing/models/response/organization-warnings.response"; import { ApiService } from "../../../abstractions/api.service"; @@ -83,4 +84,17 @@ export class OrganizationBillingApiService implements OrganizationBillingApiServ return response as string; } + + async changeSubscriptionFrequency( + organizationId: string, + request: ChangePlanFrequencyRequest, + ): Promise { + return await this.apiService.send( + "POST", + "/organizations/" + organizationId + "/billing/change-frequency", + request, + true, + false, + ); + } } From da6fb82fd8ed68a910f2558373d0e5cfcdbc0162 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 22 Jul 2025 10:21:19 -0500 Subject: [PATCH 02/21] [deps] AC: Update core-js to v3.44.0 (#15284) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- apps/cli/package.json | 2 +- package-lock.json | 10 +++++----- package.json | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/apps/cli/package.json b/apps/cli/package.json index ca7af3ec596..54855d72104 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -69,7 +69,7 @@ "browser-hrtime": "1.1.8", "chalk": "4.1.2", "commander": "11.1.0", - "core-js": "3.42.0", + "core-js": "3.44.0", "form-data": "4.0.2", "https-proxy-agent": "7.0.6", "inquirer": "8.2.6", diff --git a/package-lock.json b/package-lock.json index e44797997f1..01a9ea8c09c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -42,7 +42,7 @@ "bufferutil": "4.0.9", "chalk": "4.1.2", "commander": "11.1.0", - "core-js": "3.42.0", + "core-js": "3.44.0", "form-data": "4.0.2", "https-proxy-agent": "7.0.6", "inquirer": "8.2.6", @@ -205,7 +205,7 @@ "browser-hrtime": "1.1.8", "chalk": "4.1.2", "commander": "11.1.0", - "core-js": "3.42.0", + "core-js": "3.44.0", "form-data": "4.0.2", "https-proxy-agent": "7.0.6", "inquirer": "8.2.6", @@ -17512,9 +17512,9 @@ } }, "node_modules/core-js": { - "version": "3.42.0", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.42.0.tgz", - "integrity": "sha512-Sz4PP4ZA+Rq4II21qkNqOEDTDrCvcANId3xpIgB34NDkWc3UduWj2dqEtN9yZIq8Dk3HyPI33x9sqqU5C8sr0g==", + "version": "3.44.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.44.0.tgz", + "integrity": "sha512-aFCtd4l6GvAXwVEh3XbbVqJGHDJt0OZRa+5ePGx3LLwi12WfexqQxcsohb2wgsa/92xtl19Hd66G/L+TaAxDMw==", "hasInstallScript": true, "license": "MIT", "funding": { diff --git a/package.json b/package.json index 089ef3342e9..2cb60a6afd1 100644 --- a/package.json +++ b/package.json @@ -177,7 +177,7 @@ "bufferutil": "4.0.9", "chalk": "4.1.2", "commander": "11.1.0", - "core-js": "3.42.0", + "core-js": "3.44.0", "form-data": "4.0.2", "https-proxy-agent": "7.0.6", "inquirer": "8.2.6", From e99abb49ec26e983fa76b2bb114d6c330d0a9ce6 Mon Sep 17 00:00:00 2001 From: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> Date: Tue, 22 Jul 2025 10:30:50 -0500 Subject: [PATCH 03/21] [PM-23621] Require userId for initAccount on the key-service (#15684) * require userID for initAccount on key service * add unit test coverage * update consumer --- .../login-decryption-options.component.ts | 2 +- .../src/abstractions/key.service.ts | 7 +- libs/key-management/src/key.service.spec.ts | 101 ++++++++++++++++++ libs/key-management/src/key.service.ts | 14 ++- 4 files changed, 112 insertions(+), 12 deletions(-) diff --git a/libs/auth/src/angular/login-decryption-options/login-decryption-options.component.ts b/libs/auth/src/angular/login-decryption-options/login-decryption-options.component.ts index bbdc0106786..a2018817fed 100644 --- a/libs/auth/src/angular/login-decryption-options/login-decryption-options.component.ts +++ b/libs/auth/src/angular/login-decryption-options/login-decryption-options.component.ts @@ -249,7 +249,7 @@ export class LoginDecryptionOptionsComponent implements OnInit { } try { - const { publicKey, privateKey } = await this.keyService.initAccount(); + const { publicKey, privateKey } = await this.keyService.initAccount(this.activeAccountId); const keysRequest = new KeysRequest(publicKey, privateKey.encryptedString); await this.apiService.postAccountKeys(keysRequest); diff --git a/libs/key-management/src/abstractions/key.service.ts b/libs/key-management/src/abstractions/key.service.ts index c843d8dc872..3c0d6c8a138 100644 --- a/libs/key-management/src/abstractions/key.service.ts +++ b/libs/key-management/src/abstractions/key.service.ts @@ -386,11 +386,12 @@ export abstract class KeyService { /** * Initialize all necessary crypto keys needed for a new account. * Warning! This completely replaces any existing keys! + * @param userId The user id of the target user. * @returns The user's newly created public key, private key, and encrypted private key - * - * @throws An error if there is no user currently active. + * @throws An error if the userId is null or undefined. + * @throws An error if the user already has a user key. */ - abstract initAccount(): Promise<{ + abstract initAccount(userId: UserId): Promise<{ userKey: UserKey; publicKey: string; privateKey: EncString; diff --git a/libs/key-management/src/key.service.spec.ts b/libs/key-management/src/key.service.spec.ts index 395d668de9f..7a033792c79 100644 --- a/libs/key-management/src/key.service.spec.ts +++ b/libs/key-management/src/key.service.spec.ts @@ -1047,4 +1047,105 @@ describe("keyService", () => { }); }); }); + + describe("initAccount", () => { + let userKey: UserKey; + let mockPublicKey: string; + let mockPrivateKey: EncString; + + beforeEach(() => { + userKey = makeSymmetricCryptoKey(64); + mockPublicKey = "mockPublicKey"; + mockPrivateKey = makeEncString("mockPrivateKey"); + + keyGenerationService.createKey.mockResolvedValue(userKey); + jest.spyOn(keyService, "makeKeyPair").mockResolvedValue([mockPublicKey, mockPrivateKey]); + jest.spyOn(keyService, "setUserKey").mockResolvedValue(); + }); + + test.each([null as unknown as UserId, undefined as unknown as UserId])( + "throws when the provided userId is %s", + async (userId) => { + await expect(keyService.initAccount(userId)).rejects.toThrow("UserId is required."); + expect(keyService.setUserKey).not.toHaveBeenCalled(); + }, + ); + + it("throws when user already has a user key", async () => { + const existingUserKey = makeSymmetricCryptoKey(64); + stateProvider.singleUser.getFake(mockUserId, USER_KEY).nextState(existingUserKey); + + await expect(keyService.initAccount(mockUserId)).rejects.toThrow( + "Cannot initialize account, keys already exist.", + ); + expect(logService.error).toHaveBeenCalledWith( + "Tried to initialize account with existing user key.", + ); + expect(keyService.setUserKey).not.toHaveBeenCalled(); + }); + + it("throws when private key creation fails", async () => { + // Simulate failure + const invalidPrivateKey = new EncString( + "2.AAAw2vTUePO+CCyokcIfVw==|DTBNlJ5yVsV2Bsk3UU3H6Q==|YvFBff5gxWqM+UsFB6BKimKxhC32AtjF3IStpU1Ijwg=", + ); + invalidPrivateKey.encryptedString = null as unknown as EncryptedString; + jest.spyOn(keyService, "makeKeyPair").mockResolvedValue([mockPublicKey, invalidPrivateKey]); + + await expect(keyService.initAccount(mockUserId)).rejects.toThrow( + "Failed to create valid private key.", + ); + expect(keyService.setUserKey).not.toHaveBeenCalled(); + }); + + it("successfully initializes account with new keys", async () => { + const keyCreationSize = 512; + const privateKeyState = stateProvider.singleUser.getFake( + mockUserId, + USER_ENCRYPTED_PRIVATE_KEY, + ); + + const result = await keyService.initAccount(mockUserId); + + expect(keyGenerationService.createKey).toHaveBeenCalledWith(keyCreationSize); + expect(keyService.makeKeyPair).toHaveBeenCalledWith(userKey); + expect(keyService.setUserKey).toHaveBeenCalledWith(userKey, mockUserId); + expect(privateKeyState.nextMock).toHaveBeenCalledWith(mockPrivateKey.encryptedString); + expect(result).toEqual({ + userKey: userKey, + publicKey: mockPublicKey, + privateKey: mockPrivateKey, + }); + }); + }); + + describe("makeKeyPair", () => { + test.each([null as unknown as SymmetricCryptoKey, undefined as unknown as SymmetricCryptoKey])( + "throws when the provided key is %s", + async (key) => { + await expect(keyService.makeKeyPair(key)).rejects.toThrow( + "'key' is a required parameter and must be non-null.", + ); + }, + ); + + it("generates a key pair and returns public key and encrypted private key", async () => { + const mockKey = new SymmetricCryptoKey(new Uint8Array(64)); + const mockKeyPair: [Uint8Array, Uint8Array] = [new Uint8Array(256), new Uint8Array(256)]; + const mockPublicKeyB64 = "mockPublicKeyB64"; + const mockPrivateKeyEncString = makeEncString("encryptedPrivateKey"); + + cryptoFunctionService.rsaGenerateKeyPair.mockResolvedValue(mockKeyPair); + jest.spyOn(Utils, "fromBufferToB64").mockReturnValue(mockPublicKeyB64); + encryptService.wrapDecapsulationKey.mockResolvedValue(mockPrivateKeyEncString); + + const [publicKey, privateKey] = await keyService.makeKeyPair(mockKey); + + expect(cryptoFunctionService.rsaGenerateKeyPair).toHaveBeenCalledWith(2048); + expect(Utils.fromBufferToB64).toHaveBeenCalledWith(mockKeyPair[0]); + expect(encryptService.wrapDecapsulationKey).toHaveBeenCalledWith(mockKeyPair[1], mockKey); + expect(publicKey).toBe(mockPublicKeyB64); + expect(privateKey).toBe(mockPrivateKeyEncString); + }); + }); }); diff --git a/libs/key-management/src/key.service.ts b/libs/key-management/src/key.service.ts index fca080252f6..0f4b101d9b2 100644 --- a/libs/key-management/src/key.service.ts +++ b/libs/key-management/src/key.service.ts @@ -661,19 +661,17 @@ export class DefaultKeyService implements KeyServiceAbstraction { * Initialize all necessary crypto keys needed for a new account. * Warning! This completely replaces any existing keys! */ - async initAccount(): Promise<{ + async initAccount(userId: UserId): Promise<{ userKey: UserKey; publicKey: string; privateKey: EncString; }> { - const activeUserId = await firstValueFrom(this.stateProvider.activeUserId$); - - if (activeUserId == null) { - throw new Error("Cannot initilize an account if one is not active."); + if (userId == null) { + throw new Error("UserId is required."); } // Verify user key doesn't exist - const existingUserKey = await this.getUserKey(activeUserId); + const existingUserKey = await this.getUserKey(userId); if (existingUserKey != null) { this.logService.error("Tried to initialize account with existing user key."); @@ -686,9 +684,9 @@ export class DefaultKeyService implements KeyServiceAbstraction { throw new Error("Failed to create valid private key."); } - await this.setUserKey(userKey, activeUserId); + await this.setUserKey(userKey, userId); await this.stateProvider - .getUser(activeUserId, USER_ENCRYPTED_PRIVATE_KEY) + .getUser(userId, USER_ENCRYPTED_PRIVATE_KEY) .update(() => privateKey.encryptedString!); return { From a563e6d91000f453f5cbd8f904f397dccc96d058 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Tue, 22 Jul 2025 11:47:25 -0400 Subject: [PATCH 04/21] Add `messaging` & `messaging-internal` libraries (#15711) --- .github/CODEOWNERS | 2 + .../sync/sync-service.listener.spec.ts | 4 +- .../abstractions/messaging.service.ts | 2 +- libs/common/src/platform/messaging/index.ts | 5 +- .../common/src/platform/messaging/internal.ts | 6 +- .../messaging/subject-message.sender.spec.ts | 65 ------------------- libs/messaging-internal/README.md | 5 ++ libs/messaging-internal/eslint.config.mjs | 3 + libs/messaging-internal/jest.config.js | 10 +++ libs/messaging-internal/package.json | 11 ++++ libs/messaging-internal/project.json | 33 ++++++++++ .../src}/helpers.spec.ts | 5 +- .../src}/helpers.ts | 8 +-- libs/messaging-internal/src/index.ts | 5 ++ .../src/messaging-internal.spec.ts | 8 +++ .../src/subject-message.sender.spec.ts | 59 +++++++++++++++++ .../src}/subject-message.sender.ts | 6 +- libs/messaging-internal/tsconfig.eslint.json | 6 ++ libs/messaging-internal/tsconfig.json | 13 ++++ libs/messaging-internal/tsconfig.lib.json | 10 +++ libs/messaging-internal/tsconfig.spec.json | 10 +++ libs/messaging/README.md | 5 ++ libs/messaging/eslint.config.mjs | 3 + libs/messaging/jest.config.js | 10 +++ libs/messaging/package.json | 11 ++++ libs/messaging/project.json | 33 ++++++++++ libs/messaging/src/index.ts | 4 ++ libs/messaging/src/is-external-message.ts | 5 ++ .../src}/message.listener.spec.ts | 26 ++++---- .../src}/message.listener.ts | 0 .../src}/message.sender.ts | 0 libs/messaging/src/messaging.spec.ts | 8 +++ .../messaging => messaging/src}/types.ts | 0 libs/messaging/tsconfig.eslint.json | 6 ++ libs/messaging/tsconfig.json | 13 ++++ libs/messaging/tsconfig.lib.json | 16 +++++ libs/messaging/tsconfig.spec.json | 17 +++++ package-lock.json | 17 +++++ tsconfig.base.json | 2 + 39 files changed, 347 insertions(+), 105 deletions(-) delete mode 100644 libs/common/src/platform/messaging/subject-message.sender.spec.ts create mode 100644 libs/messaging-internal/README.md create mode 100644 libs/messaging-internal/eslint.config.mjs create mode 100644 libs/messaging-internal/jest.config.js create mode 100644 libs/messaging-internal/package.json create mode 100644 libs/messaging-internal/project.json rename libs/{common/src/platform/messaging => messaging-internal/src}/helpers.spec.ts (90%) rename libs/{common/src/platform/messaging => messaging-internal/src}/helpers.ts (65%) create mode 100644 libs/messaging-internal/src/index.ts create mode 100644 libs/messaging-internal/src/messaging-internal.spec.ts create mode 100644 libs/messaging-internal/src/subject-message.sender.spec.ts rename libs/{common/src/platform/messaging => messaging-internal/src}/subject-message.sender.ts (77%) create mode 100644 libs/messaging-internal/tsconfig.eslint.json create mode 100644 libs/messaging-internal/tsconfig.json create mode 100644 libs/messaging-internal/tsconfig.lib.json create mode 100644 libs/messaging-internal/tsconfig.spec.json create mode 100644 libs/messaging/README.md create mode 100644 libs/messaging/eslint.config.mjs create mode 100644 libs/messaging/jest.config.js create mode 100644 libs/messaging/package.json create mode 100644 libs/messaging/project.json create mode 100644 libs/messaging/src/index.ts create mode 100644 libs/messaging/src/is-external-message.ts rename libs/{common/src/platform/messaging => messaging/src}/message.listener.spec.ts (55%) rename libs/{common/src/platform/messaging => messaging/src}/message.listener.ts (100%) rename libs/{common/src/platform/messaging => messaging/src}/message.sender.ts (100%) create mode 100644 libs/messaging/src/messaging.spec.ts rename libs/{common/src/platform/messaging => messaging/src}/types.ts (100%) create mode 100644 libs/messaging/tsconfig.eslint.json create mode 100644 libs/messaging/tsconfig.json create mode 100644 libs/messaging/tsconfig.lib.json create mode 100644 libs/messaging/tsconfig.spec.json diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 7d7fec2a5ea..203c7ae7607 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -94,6 +94,8 @@ libs/platform @bitwarden/team-platform-dev libs/storage-core @bitwarden/team-platform-dev libs/logging @bitwarden/team-platform-dev libs/storage-test-utils @bitwarden/team-platform-dev +libs/messaging @bitwarden/team-platform-dev +libs/messaging-internal @bitwarden/team-platform-dev # Web utils used across app and connectors apps/web/src/utils/ @bitwarden/team-platform-dev # Web core and shared files diff --git a/apps/browser/src/platform/sync/sync-service.listener.spec.ts b/apps/browser/src/platform/sync/sync-service.listener.spec.ts index dc0674a7ae5..383586c0cd0 100644 --- a/apps/browser/src/platform/sync/sync-service.listener.spec.ts +++ b/apps/browser/src/platform/sync/sync-service.listener.spec.ts @@ -3,10 +3,8 @@ import { Subject, firstValueFrom } from "rxjs"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { MessageListener, MessageSender } from "@bitwarden/common/platform/messaging"; -// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. -// eslint-disable-next-line no-restricted-imports -import { tagAsExternal } from "@bitwarden/common/platform/messaging/helpers"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; +import { tagAsExternal } from "@bitwarden/messaging-internal"; import { FullSyncMessage } from "./foreground-sync.service"; import { FULL_SYNC_FINISHED, SyncServiceListener } from "./sync-service.listener"; diff --git a/libs/common/src/platform/abstractions/messaging.service.ts b/libs/common/src/platform/abstractions/messaging.service.ts index f24279f932a..3520d9352ef 100644 --- a/libs/common/src/platform/abstractions/messaging.service.ts +++ b/libs/common/src/platform/abstractions/messaging.service.ts @@ -1,3 +1,3 @@ // Export the new message sender as the legacy MessagingService to minimize changes in the initial PR, // team specific PR's will come after. -export { MessageSender as MessagingService } from "../messaging/message.sender"; +export { MessageSender as MessagingService } from "@bitwarden/messaging"; diff --git a/libs/common/src/platform/messaging/index.ts b/libs/common/src/platform/messaging/index.ts index a9b4eca5ae8..5d452f32b31 100644 --- a/libs/common/src/platform/messaging/index.ts +++ b/libs/common/src/platform/messaging/index.ts @@ -1,4 +1 @@ -export { MessageListener } from "./message.listener"; -export { MessageSender } from "./message.sender"; -export { Message, CommandDefinition } from "./types"; -export { isExternalMessage } from "./helpers"; +export * from "@bitwarden/messaging"; diff --git a/libs/common/src/platform/messaging/internal.ts b/libs/common/src/platform/messaging/internal.ts index 08763d48bc5..9fe261f2264 100644 --- a/libs/common/src/platform/messaging/internal.ts +++ b/libs/common/src/platform/messaging/internal.ts @@ -1,5 +1 @@ -// Built in implementations -export { SubjectMessageSender } from "./subject-message.sender"; - -// Helpers meant to be used only by other implementations -export { tagAsExternal, getCommand } from "./helpers"; +export * from "@bitwarden/messaging-internal"; diff --git a/libs/common/src/platform/messaging/subject-message.sender.spec.ts b/libs/common/src/platform/messaging/subject-message.sender.spec.ts deleted file mode 100644 index 4278fca7bc1..00000000000 --- a/libs/common/src/platform/messaging/subject-message.sender.spec.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { Subject } from "rxjs"; - -import { subscribeTo } from "../../../spec/observable-tracker"; - -import { SubjectMessageSender } from "./internal"; -import { MessageSender } from "./message.sender"; -import { Message, CommandDefinition } from "./types"; - -describe("SubjectMessageSender", () => { - const subject = new Subject>(); - const subjectObservable = subject.asObservable(); - - const sut: MessageSender = new SubjectMessageSender(subject); - - describe("send", () => { - it("will send message with command from message definition", async () => { - const commandDefinition = new CommandDefinition<{ test: number }>("myCommand"); - - const tracker = subscribeTo(subjectObservable); - const pausePromise = tracker.pauseUntilReceived(1); - - sut.send(commandDefinition, { test: 1 }); - - await pausePromise; - - expect(tracker.emissions[0]).toEqual({ command: "myCommand", test: 1 }); - }); - - it("will send message with command from normal string", async () => { - const tracker = subscribeTo(subjectObservable); - const pausePromise = tracker.pauseUntilReceived(1); - - sut.send("myCommand", { test: 1 }); - - await pausePromise; - - expect(tracker.emissions[0]).toEqual({ command: "myCommand", test: 1 }); - }); - - it("will send message with object even if payload not given", async () => { - const tracker = subscribeTo(subjectObservable); - const pausePromise = tracker.pauseUntilReceived(1); - - sut.send("myCommand"); - - await pausePromise; - - expect(tracker.emissions[0]).toEqual({ command: "myCommand" }); - }); - - it.each([null, undefined])( - "will send message with object even if payload is null-ish (%s)", - async (payloadValue) => { - const tracker = subscribeTo(subjectObservable); - const pausePromise = tracker.pauseUntilReceived(1); - - sut.send("myCommand", payloadValue); - - await pausePromise; - - expect(tracker.emissions[0]).toEqual({ command: "myCommand" }); - }, - ); - }); -}); diff --git a/libs/messaging-internal/README.md b/libs/messaging-internal/README.md new file mode 100644 index 00000000000..a2f36138ad7 --- /dev/null +++ b/libs/messaging-internal/README.md @@ -0,0 +1,5 @@ +# messaging-internal + +Owned by: platform + +Internal details to accompany @bitwarden/messaging this library should not be consumed in non-platform code. diff --git a/libs/messaging-internal/eslint.config.mjs b/libs/messaging-internal/eslint.config.mjs new file mode 100644 index 00000000000..9c37d10e3ff --- /dev/null +++ b/libs/messaging-internal/eslint.config.mjs @@ -0,0 +1,3 @@ +import baseConfig from "../../eslint.config.mjs"; + +export default [...baseConfig]; diff --git a/libs/messaging-internal/jest.config.js b/libs/messaging-internal/jest.config.js new file mode 100644 index 00000000000..152244f6603 --- /dev/null +++ b/libs/messaging-internal/jest.config.js @@ -0,0 +1,10 @@ +module.exports = { + displayName: "messaging-internal", + preset: "../../jest.preset.js", + testEnvironment: "node", + transform: { + "^.+\\.[tj]s$": ["ts-jest", { tsconfig: "/tsconfig.spec.json" }], + }, + moduleFileExtensions: ["ts", "js", "html"], + coverageDirectory: "../../coverage/libs/messaging-internal", +}; diff --git a/libs/messaging-internal/package.json b/libs/messaging-internal/package.json new file mode 100644 index 00000000000..7a0a13d2d67 --- /dev/null +++ b/libs/messaging-internal/package.json @@ -0,0 +1,11 @@ +{ + "name": "@bitwarden/messaging-internal", + "version": "0.0.1", + "description": "Internal details to accompany @bitwarden/messaging this library should not be consumed in non-platform code.", + "private": true, + "type": "commonjs", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "license": "GPL-3.0", + "author": "platform" +} diff --git a/libs/messaging-internal/project.json b/libs/messaging-internal/project.json new file mode 100644 index 00000000000..ad55cde5c20 --- /dev/null +++ b/libs/messaging-internal/project.json @@ -0,0 +1,33 @@ +{ + "name": "messaging-internal", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/messaging-internal/src", + "projectType": "library", + "tags": [], + "targets": { + "build": { + "executor": "@nx/js:tsc", + "outputs": ["{options.outputPath}"], + "options": { + "outputPath": "dist/libs/messaging-internal", + "main": "libs/messaging-internal/src/index.ts", + "tsConfig": "libs/messaging-internal/tsconfig.lib.json", + "assets": ["libs/messaging-internal/*.md"] + } + }, + "lint": { + "executor": "@nx/eslint:lint", + "outputs": ["{options.outputFile}"], + "options": { + "lintFilePatterns": ["libs/messaging-internal/**/*.ts"] + } + }, + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "libs/messaging-internal/jest.config.js" + } + } + } +} diff --git a/libs/common/src/platform/messaging/helpers.spec.ts b/libs/messaging-internal/src/helpers.spec.ts similarity index 90% rename from libs/common/src/platform/messaging/helpers.spec.ts rename to libs/messaging-internal/src/helpers.spec.ts index 8839a542ffc..5a97ff959cc 100644 --- a/libs/common/src/platform/messaging/helpers.spec.ts +++ b/libs/messaging-internal/src/helpers.spec.ts @@ -1,7 +1,8 @@ import { Subject, firstValueFrom } from "rxjs"; -import { getCommand, isExternalMessage, tagAsExternal } from "./helpers"; -import { Message, CommandDefinition } from "./types"; +import { CommandDefinition, isExternalMessage, Message } from "@bitwarden/messaging"; + +import { getCommand, tagAsExternal } from "./helpers"; describe("helpers", () => { describe("getCommand", () => { diff --git a/libs/common/src/platform/messaging/helpers.ts b/libs/messaging-internal/src/helpers.ts similarity index 65% rename from libs/common/src/platform/messaging/helpers.ts rename to libs/messaging-internal/src/helpers.ts index e7521ea42a2..00231b455b7 100644 --- a/libs/common/src/platform/messaging/helpers.ts +++ b/libs/messaging-internal/src/helpers.ts @@ -1,6 +1,6 @@ import { map } from "rxjs"; -import { CommandDefinition } from "./types"; +import { CommandDefinition, EXTERNAL_SOURCE_TAG } from "@bitwarden/messaging"; export const getCommand = ( commandDefinition: CommandDefinition> | string, @@ -12,12 +12,6 @@ export const getCommand = ( } }; -export const EXTERNAL_SOURCE_TAG = Symbol("externalSource"); - -export const isExternalMessage = (message: Record) => { - return message?.[EXTERNAL_SOURCE_TAG] === true; -}; - export const tagAsExternal = >() => { return map((message: T) => { return Object.assign(message, { [EXTERNAL_SOURCE_TAG]: true }); diff --git a/libs/messaging-internal/src/index.ts b/libs/messaging-internal/src/index.ts new file mode 100644 index 00000000000..08763d48bc5 --- /dev/null +++ b/libs/messaging-internal/src/index.ts @@ -0,0 +1,5 @@ +// Built in implementations +export { SubjectMessageSender } from "./subject-message.sender"; + +// Helpers meant to be used only by other implementations +export { tagAsExternal, getCommand } from "./helpers"; diff --git a/libs/messaging-internal/src/messaging-internal.spec.ts b/libs/messaging-internal/src/messaging-internal.spec.ts new file mode 100644 index 00000000000..b2b50a218bd --- /dev/null +++ b/libs/messaging-internal/src/messaging-internal.spec.ts @@ -0,0 +1,8 @@ +import * as lib from "./index"; + +describe("messaging-internal", () => { + // This test will fail until something is exported from index.ts + it("should work", () => { + expect(lib).toBeDefined(); + }); +}); diff --git a/libs/messaging-internal/src/subject-message.sender.spec.ts b/libs/messaging-internal/src/subject-message.sender.spec.ts new file mode 100644 index 00000000000..e3e5305d1b2 --- /dev/null +++ b/libs/messaging-internal/src/subject-message.sender.spec.ts @@ -0,0 +1,59 @@ +import { bufferCount, firstValueFrom, Subject } from "rxjs"; + +import { CommandDefinition, Message } from "@bitwarden/messaging"; + +import { SubjectMessageSender } from "./subject-message.sender"; + +describe("SubjectMessageSender", () => { + const subject = new Subject>(); + const subjectObservable = subject.asObservable(); + + const sut = new SubjectMessageSender(subject); + + describe("send", () => { + it("will send message with command from message definition", async () => { + const commandDefinition = new CommandDefinition<{ test: number }>("myCommand"); + + const emissionsPromise = firstValueFrom(subjectObservable.pipe(bufferCount(1))); + + sut.send(commandDefinition, { test: 1 }); + + const emissions = await emissionsPromise; + + expect(emissions[0]).toEqual({ command: "myCommand", test: 1 }); + }); + + it("will send message with command from normal string", async () => { + const emissionsPromise = firstValueFrom(subjectObservable.pipe(bufferCount(1))); + + sut.send("myCommand", { test: 1 }); + + const emissions = await emissionsPromise; + + expect(emissions[0]).toEqual({ command: "myCommand", test: 1 }); + }); + + it("will send message with object even if payload not given", async () => { + const emissionsPromise = firstValueFrom(subjectObservable.pipe(bufferCount(1))); + + sut.send("myCommand"); + + const emissions = await emissionsPromise; + + expect(emissions[0]).toEqual({ command: "myCommand" }); + }); + + it.each([null, undefined])( + "will send message with object even if payload is null-ish (%s)", + async (payloadValue) => { + const emissionsPromise = firstValueFrom(subjectObservable.pipe(bufferCount(1))); + + sut.send("myCommand", payloadValue); + + const emissions = await emissionsPromise; + + expect(emissions[0]).toEqual({ command: "myCommand" }); + }, + ); + }); +}); diff --git a/libs/common/src/platform/messaging/subject-message.sender.ts b/libs/messaging-internal/src/subject-message.sender.ts similarity index 77% rename from libs/common/src/platform/messaging/subject-message.sender.ts rename to libs/messaging-internal/src/subject-message.sender.ts index 170f8a24c6f..e8df5913b01 100644 --- a/libs/common/src/platform/messaging/subject-message.sender.ts +++ b/libs/messaging-internal/src/subject-message.sender.ts @@ -1,8 +1,8 @@ import { Subject } from "rxjs"; -import { getCommand } from "./internal"; -import { MessageSender } from "./message.sender"; -import { Message, CommandDefinition } from "./types"; +import { CommandDefinition, Message, MessageSender } from "@bitwarden/messaging"; + +import { getCommand } from "./helpers"; export class SubjectMessageSender implements MessageSender { constructor(private readonly messagesSubject: Subject>>) {} diff --git a/libs/messaging-internal/tsconfig.eslint.json b/libs/messaging-internal/tsconfig.eslint.json new file mode 100644 index 00000000000..3daf120441a --- /dev/null +++ b/libs/messaging-internal/tsconfig.eslint.json @@ -0,0 +1,6 @@ +{ + "extends": "../../tsconfig.base.json", + "files": [], + "include": ["src/**/*.ts", "src/**/*.js"], + "exclude": ["**/build", "**/dist"] +} diff --git a/libs/messaging-internal/tsconfig.json b/libs/messaging-internal/tsconfig.json new file mode 100644 index 00000000000..62ebbd94647 --- /dev/null +++ b/libs/messaging-internal/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/libs/messaging-internal/tsconfig.lib.json b/libs/messaging-internal/tsconfig.lib.json new file mode 100644 index 00000000000..9cbf6736007 --- /dev/null +++ b/libs/messaging-internal/tsconfig.lib.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "declaration": true, + "types": ["node"] + }, + "include": ["src/**/*.ts"], + "exclude": ["jest.config.js", "src/**/*.spec.ts"] +} diff --git a/libs/messaging-internal/tsconfig.spec.json b/libs/messaging-internal/tsconfig.spec.json new file mode 100644 index 00000000000..1275f148a18 --- /dev/null +++ b/libs/messaging-internal/tsconfig.spec.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "module": "commonjs", + "moduleResolution": "node10", + "types": ["jest", "node"] + }, + "include": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts", "src/**/*.d.ts"] +} diff --git a/libs/messaging/README.md b/libs/messaging/README.md new file mode 100644 index 00000000000..98eb96a5a40 --- /dev/null +++ b/libs/messaging/README.md @@ -0,0 +1,5 @@ +# messaging + +Owned by: platform + +Services for sending and recieving messages from different contexts of the same application. diff --git a/libs/messaging/eslint.config.mjs b/libs/messaging/eslint.config.mjs new file mode 100644 index 00000000000..9c37d10e3ff --- /dev/null +++ b/libs/messaging/eslint.config.mjs @@ -0,0 +1,3 @@ +import baseConfig from "../../eslint.config.mjs"; + +export default [...baseConfig]; diff --git a/libs/messaging/jest.config.js b/libs/messaging/jest.config.js new file mode 100644 index 00000000000..f0450499e31 --- /dev/null +++ b/libs/messaging/jest.config.js @@ -0,0 +1,10 @@ +module.exports = { + displayName: "messaging", + preset: "../../jest.preset.js", + testEnvironment: "node", + transform: { + "^.+\\.[tj]s$": ["ts-jest", { tsconfig: "/tsconfig.spec.json" }], + }, + moduleFileExtensions: ["ts", "js", "html"], + coverageDirectory: "../../coverage/libs/messaging", +}; diff --git a/libs/messaging/package.json b/libs/messaging/package.json new file mode 100644 index 00000000000..01c8d7cb0e7 --- /dev/null +++ b/libs/messaging/package.json @@ -0,0 +1,11 @@ +{ + "name": "@bitwarden/messaging", + "version": "0.0.1", + "description": "Services for sending and recieving messages from different contexts of the same application.", + "private": true, + "type": "commonjs", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "license": "GPL-3.0", + "author": "platform" +} diff --git a/libs/messaging/project.json b/libs/messaging/project.json new file mode 100644 index 00000000000..f00e0bd2dc9 --- /dev/null +++ b/libs/messaging/project.json @@ -0,0 +1,33 @@ +{ + "name": "messaging", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/messaging/src", + "projectType": "library", + "tags": [], + "targets": { + "build": { + "executor": "@nx/js:tsc", + "outputs": ["{options.outputPath}"], + "options": { + "outputPath": "dist/libs/messaging", + "main": "libs/messaging/src/index.ts", + "tsConfig": "libs/messaging/tsconfig.lib.json", + "assets": ["libs/messaging/*.md"] + } + }, + "lint": { + "executor": "@nx/eslint:lint", + "outputs": ["{options.outputFile}"], + "options": { + "lintFilePatterns": ["libs/messaging/**/*.ts"] + } + }, + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "libs/messaging/jest.config.js" + } + } + } +} diff --git a/libs/messaging/src/index.ts b/libs/messaging/src/index.ts new file mode 100644 index 00000000000..9090ff581c1 --- /dev/null +++ b/libs/messaging/src/index.ts @@ -0,0 +1,4 @@ +export { MessageListener } from "./message.listener"; +export { MessageSender } from "./message.sender"; +export { Message, CommandDefinition } from "./types"; +export { isExternalMessage, EXTERNAL_SOURCE_TAG } from "./is-external-message"; diff --git a/libs/messaging/src/is-external-message.ts b/libs/messaging/src/is-external-message.ts new file mode 100644 index 00000000000..46775cb14d6 --- /dev/null +++ b/libs/messaging/src/is-external-message.ts @@ -0,0 +1,5 @@ +export const EXTERNAL_SOURCE_TAG = Symbol("externalSource"); + +export const isExternalMessage = (message: Record) => { + return message?.[EXTERNAL_SOURCE_TAG] === true; +}; diff --git a/libs/common/src/platform/messaging/message.listener.spec.ts b/libs/messaging/src/message.listener.spec.ts similarity index 55% rename from libs/common/src/platform/messaging/message.listener.spec.ts rename to libs/messaging/src/message.listener.spec.ts index 98bbf1fdc82..19787c6feae 100644 --- a/libs/common/src/platform/messaging/message.listener.spec.ts +++ b/libs/messaging/src/message.listener.spec.ts @@ -1,6 +1,4 @@ -import { Subject } from "rxjs"; - -import { subscribeTo } from "../../../spec/observable-tracker"; +import { bufferCount, firstValueFrom, Subject } from "rxjs"; import { MessageListener } from "./message.listener"; import { Message, CommandDefinition } from "./types"; @@ -13,35 +11,33 @@ describe("MessageListener", () => { describe("allMessages$", () => { it("runs on all nexts", async () => { - const tracker = subscribeTo(sut.allMessages$); - - const pausePromise = tracker.pauseUntilReceived(2); + const emissionsPromise = firstValueFrom(sut.allMessages$.pipe(bufferCount(2))); subject.next({ command: "command1", test: 1 }); subject.next({ command: "command2", test: 2 }); - await pausePromise; + const emissions = await emissionsPromise; - expect(tracker.emissions[0]).toEqual({ command: "command1", test: 1 }); - expect(tracker.emissions[1]).toEqual({ command: "command2", test: 2 }); + expect(emissions[0]).toEqual({ command: "command1", test: 1 }); + expect(emissions[1]).toEqual({ command: "command2", test: 2 }); }); }); describe("messages$", () => { it("runs on only my commands", async () => { - const tracker = subscribeTo(sut.messages$(testCommandDefinition)); - - const pausePromise = tracker.pauseUntilReceived(2); + const emissionsPromise = firstValueFrom( + sut.messages$(testCommandDefinition).pipe(bufferCount(2)), + ); subject.next({ command: "notMyCommand", test: 1 }); subject.next({ command: "myCommand", test: 2 }); subject.next({ command: "myCommand", test: 3 }); subject.next({ command: "notMyCommand", test: 4 }); - await pausePromise; + const emissions = await emissionsPromise; - expect(tracker.emissions[0]).toEqual({ command: "myCommand", test: 2 }); - expect(tracker.emissions[1]).toEqual({ command: "myCommand", test: 3 }); + expect(emissions[0]).toEqual({ command: "myCommand", test: 2 }); + expect(emissions[1]).toEqual({ command: "myCommand", test: 3 }); }); }); }); diff --git a/libs/common/src/platform/messaging/message.listener.ts b/libs/messaging/src/message.listener.ts similarity index 100% rename from libs/common/src/platform/messaging/message.listener.ts rename to libs/messaging/src/message.listener.ts diff --git a/libs/common/src/platform/messaging/message.sender.ts b/libs/messaging/src/message.sender.ts similarity index 100% rename from libs/common/src/platform/messaging/message.sender.ts rename to libs/messaging/src/message.sender.ts diff --git a/libs/messaging/src/messaging.spec.ts b/libs/messaging/src/messaging.spec.ts new file mode 100644 index 00000000000..170b24750c5 --- /dev/null +++ b/libs/messaging/src/messaging.spec.ts @@ -0,0 +1,8 @@ +import * as lib from "./index"; + +describe("messaging", () => { + // This test will fail until something is exported from index.ts + it("should work", () => { + expect(lib).toBeDefined(); + }); +}); diff --git a/libs/common/src/platform/messaging/types.ts b/libs/messaging/src/types.ts similarity index 100% rename from libs/common/src/platform/messaging/types.ts rename to libs/messaging/src/types.ts diff --git a/libs/messaging/tsconfig.eslint.json b/libs/messaging/tsconfig.eslint.json new file mode 100644 index 00000000000..3daf120441a --- /dev/null +++ b/libs/messaging/tsconfig.eslint.json @@ -0,0 +1,6 @@ +{ + "extends": "../../tsconfig.base.json", + "files": [], + "include": ["src/**/*.ts", "src/**/*.js"], + "exclude": ["**/build", "**/dist"] +} diff --git a/libs/messaging/tsconfig.json b/libs/messaging/tsconfig.json new file mode 100644 index 00000000000..62ebbd94647 --- /dev/null +++ b/libs/messaging/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/libs/messaging/tsconfig.lib.json b/libs/messaging/tsconfig.lib.json new file mode 100644 index 00000000000..1f3b89d988e --- /dev/null +++ b/libs/messaging/tsconfig.lib.json @@ -0,0 +1,16 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "declaration": true, + "types": ["node"] + }, + "include": [ + "src/**/*.ts", + "../messaging-internal/src/subject-message.sender.spec.ts", + "../messaging-internal/src/subject-message.sender.ts", + "../messaging-internal/src/helpers.spec.ts", + "../messaging-internal/src/helpers.ts" + ], + "exclude": ["jest.config.js", "src/**/*.spec.ts"] +} diff --git a/libs/messaging/tsconfig.spec.json b/libs/messaging/tsconfig.spec.json new file mode 100644 index 00000000000..2e5b192faff --- /dev/null +++ b/libs/messaging/tsconfig.spec.json @@ -0,0 +1,17 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "module": "commonjs", + "moduleResolution": "node10", + "types": ["jest", "node"] + }, + "include": [ + "jest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.d.ts", + "../messaging-internal/src/subject-message.sender.spec.ts", + "../messaging-internal/src/helpers.spec.ts" + ] +} diff --git a/package-lock.json b/package-lock.json index 01a9ea8c09c..fbbc4c25b44 100644 --- a/package-lock.json +++ b/package-lock.json @@ -352,6 +352,15 @@ "version": "0.0.1", "license": "GPL-3.0" }, + "libs/messaging": { + "name": "@bitwarden/messaging", + "version": "0.0.1", + "license": "GPL-3.0" + }, + "libs/messaging-internal": { + "version": "0.0.1", + "license": "GPL-3.0" + }, "libs/node": { "name": "@bitwarden/node", "version": "0.0.0", @@ -4591,6 +4600,14 @@ "resolved": "libs/logging", "link": true }, + "node_modules/@bitwarden/messaging": { + "resolved": "libs/messaging", + "link": true + }, + "node_modules/@bitwarden/messaging-internal": { + "resolved": "libs/messaging-internal", + "link": true + }, "node_modules/@bitwarden/node": { "resolved": "libs/node", "link": true diff --git a/tsconfig.base.json b/tsconfig.base.json index c462ab97d37..478fce4bfd8 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -38,6 +38,8 @@ "@bitwarden/key-management": ["./libs/key-management/src"], "@bitwarden/key-management-ui": ["./libs/key-management-ui/src"], "@bitwarden/logging": ["libs/logging/src"], + "@bitwarden/messaging": ["libs/messaging/src/index.ts"], + "@bitwarden/messaging-internal": ["libs/messaging-internal/src/index.ts"], "@bitwarden/node/*": ["./libs/node/src/*"], "@bitwarden/nx-plugin": ["libs/nx-plugin/src/index.ts"], "@bitwarden/platform": ["./libs/platform/src"], From 9839087b00a3de91e8b4a87cce4d9e74a201d95b Mon Sep 17 00:00:00 2001 From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> Date: Tue, 22 Jul 2025 11:30:22 -0500 Subject: [PATCH 05/21] Only return ciphers when they exist. (#15716) conditionals within the template are checking for an empty array rather than an empty ciphers property. --- .../vault-list-items-container.component.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.ts b/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.ts index 5fc1c43210c..5a08ed3002b 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.ts @@ -146,14 +146,16 @@ export class VaultListItemsContainerComponent implements AfterViewInit { ciphers: PopupCipherViewLike[]; }[] >(() => { + const ciphers = this.ciphers(); + // Not grouping by type, return a single group with all ciphers - if (!this.groupByType()) { - return [{ ciphers: this.ciphers() }]; + if (!this.groupByType() && ciphers.length > 0) { + return [{ ciphers }]; } const groups: Record = {}; - this.ciphers().forEach((cipher) => { + ciphers.forEach((cipher) => { let groupKey = "all"; switch (CipherViewLikeUtils.getType(cipher)) { case CipherType.Card: From 6aa59d5ba795a5ddd513a913ce4ea93129c65946 Mon Sep 17 00:00:00 2001 From: Andy Pixley <3723676+pixman20@users.noreply.github.com> Date: Tue, 22 Jul 2025 12:46:02 -0400 Subject: [PATCH 06/21] [BRE-831] Fixing PR target permissions (#15729) --- .github/workflows/build-browser-target.yml | 3 ++- .github/workflows/build-desktop-target.yml | 3 ++- .github/workflows/build-web-target.yml | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-browser-target.yml b/.github/workflows/build-browser-target.yml index ef3beef4b8b..e89a41c1009 100644 --- a/.github/workflows/build-browser-target.yml +++ b/.github/workflows/build-browser-target.yml @@ -38,6 +38,7 @@ jobs: uses: ./.github/workflows/build-browser.yml secrets: inherit permissions: - contents: read + contents: write + pull-requests: write id-token: write diff --git a/.github/workflows/build-desktop-target.yml b/.github/workflows/build-desktop-target.yml index 31ac819a3e6..96a0e6880f8 100644 --- a/.github/workflows/build-desktop-target.yml +++ b/.github/workflows/build-desktop-target.yml @@ -38,6 +38,7 @@ jobs: uses: ./.github/workflows/build-desktop.yml secrets: inherit permissions: - contents: read + contents: write + pull-requests: write id-token: write diff --git a/.github/workflows/build-web-target.yml b/.github/workflows/build-web-target.yml index b1055885400..2f9e201ac60 100644 --- a/.github/workflows/build-web-target.yml +++ b/.github/workflows/build-web-target.yml @@ -37,7 +37,8 @@ jobs: uses: ./.github/workflows/build-web.yml secrets: inherit permissions: - contents: read + contents: write + pull-requests: write id-token: write security-events: write From 8aeeb92958bb6bd9396444ba08ccfdd52b730390 Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Tue, 22 Jul 2025 18:48:00 +0200 Subject: [PATCH 07/21] [PM-24030] Migrate abstract services in libs/common strict TS (#15727) Migrates the abstract classes in libs/common to be strict ts compatible. Primarily by adding abstract to every field and converting it to a function syntax instead of lambda. --- libs/common/src/abstractions/api.service.ts | 446 ++++++++++-------- .../event/event-collection.service.ts | 10 +- .../event/event-upload.service.ts | 4 +- .../org-domain-api.service.abstraction.ts | 22 +- .../org-domain.service.abstraction.ts | 16 +- .../organization-api.service.abstraction.ts | 94 ++-- .../organization.service.abstraction.ts | 19 +- .../abstractions/provider.service.ts | 10 +- .../provider-api.service.abstraction.ts | 24 +- .../src/auth/abstractions/account.service.ts | 14 +- .../abstractions/anonymous-hub.service.ts | 6 +- .../src/auth/abstractions/avatar.service.ts | 4 +- .../devices-api.service.abstraction.ts | 24 +- .../src/auth/abstractions/token.service.ts | 60 ++- ...er-verification-api.service.abstraction.ts | 10 +- .../user-verification.service.abstraction.ts | 22 +- .../webauthn-login-api.service.abstraction.ts | 6 +- ...authn-login-prf-key.service.abstraction.ts | 6 +- .../webauthn-login.service.abstraction.ts | 10 +- ...account-billing-api.service.abstraction.ts | 11 +- .../billing-account-profile-state.service.ts | 2 - .../billing-api.service.abstraction.ts | 67 ++- .../organization-billing.service.ts | 20 +- .../device-trust.service.abstraction.ts | 32 +- .../vault-timeout-settings.service.ts | 18 +- .../abstractions/vault-timeout.service.ts | 8 +- ...fido2-authenticator.service.abstraction.ts | 12 +- .../fido2/fido2-client.service.abstraction.ts | 12 +- ...ido2-user-interface.service.abstraction.ts | 22 +- .../platform/abstractions/state.service.ts | 30 +- .../password-strength.service.abstraction.ts | 8 +- .../services/send-api.service.abstraction.ts | 37 +- .../send/services/send.service.abstraction.ts | 28 +- .../src/vault/abstractions/cipher.service.ts | 2 - .../file-upload/cipher-file-upload.service.ts | 6 +- .../folder/folder-api.service.abstraction.ts | 13 +- .../folder/folder.service.abstraction.ts | 32 +- .../src/vault/abstractions/search.service.ts | 22 +- .../vault-settings/vault-settings.service.ts | 20 +- 39 files changed, 595 insertions(+), 614 deletions(-) diff --git a/libs/common/src/abstractions/api.service.ts b/libs/common/src/abstractions/api.service.ts index 4969e87f1c6..015a742c1ac 100644 --- a/libs/common/src/abstractions/api.service.ts +++ b/libs/common/src/abstractions/api.service.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore // This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. // eslint-disable-next-line no-restricted-imports import { @@ -128,7 +126,7 @@ import { OptionalCipherResponse } from "../vault/models/response/optional-cipher * of this decision please read https://contributing.bitwarden.com/architecture/adr/refactor-api-service. */ export abstract class ApiService { - send: ( + abstract send( method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH", path: string, body: any, @@ -136,196 +134,225 @@ export abstract class ApiService { hasResponse: boolean, apiUrl?: string | null, alterHeaders?: (headers: Headers) => void, - ) => Promise; + ): Promise; - postIdentityToken: ( + abstract postIdentityToken( request: | PasswordTokenRequest | SsoTokenRequest | UserApiTokenRequest | WebAuthnLoginTokenRequest, - ) => Promise< + ): Promise< IdentityTokenResponse | IdentityTwoFactorResponse | IdentityDeviceVerificationResponse >; - refreshIdentityToken: () => Promise; + abstract refreshIdentityToken(): Promise; - getProfile: () => Promise; - getUserSubscription: () => Promise; - getTaxInfo: () => Promise; - putProfile: (request: UpdateProfileRequest) => Promise; - putAvatar: (request: UpdateAvatarRequest) => Promise; - putTaxInfo: (request: TaxInfoUpdateRequest) => Promise; - postPrelogin: (request: PreloginRequest) => Promise; - postEmailToken: (request: EmailTokenRequest) => Promise; - postEmail: (request: EmailRequest) => Promise; - postSetKeyConnectorKey: (request: SetKeyConnectorKeyRequest) => Promise; - postSecurityStamp: (request: SecretVerificationRequest) => Promise; - getAccountRevisionDate: () => Promise; - postPasswordHint: (request: PasswordHintRequest) => Promise; - postPremium: (data: FormData) => Promise; - postReinstatePremium: () => Promise; - postAccountStorage: (request: StorageRequest) => Promise; - postAccountPayment: (request: PaymentRequest) => Promise; - postAccountLicense: (data: FormData) => Promise; - postAccountKeys: (request: KeysRequest) => Promise; - postAccountVerifyEmail: () => Promise; - postAccountVerifyEmailToken: (request: VerifyEmailRequest) => Promise; - postAccountRecoverDelete: (request: DeleteRecoverRequest) => Promise; - postAccountRecoverDeleteToken: (request: VerifyDeleteRecoverRequest) => Promise; - postAccountKdf: (request: KdfRequest) => Promise; - postUserApiKey: (id: string, request: SecretVerificationRequest) => Promise; - postUserRotateApiKey: (id: string, request: SecretVerificationRequest) => Promise; - postConvertToKeyConnector: () => Promise; + abstract getProfile(): Promise; + abstract getUserSubscription(): Promise; + abstract getTaxInfo(): Promise; + abstract putProfile(request: UpdateProfileRequest): Promise; + abstract putAvatar(request: UpdateAvatarRequest): Promise; + abstract putTaxInfo(request: TaxInfoUpdateRequest): Promise; + abstract postPrelogin(request: PreloginRequest): Promise; + abstract postEmailToken(request: EmailTokenRequest): Promise; + abstract postEmail(request: EmailRequest): Promise; + abstract postSetKeyConnectorKey(request: SetKeyConnectorKeyRequest): Promise; + abstract postSecurityStamp(request: SecretVerificationRequest): Promise; + abstract getAccountRevisionDate(): Promise; + abstract postPasswordHint(request: PasswordHintRequest): Promise; + abstract postPremium(data: FormData): Promise; + abstract postReinstatePremium(): Promise; + abstract postAccountStorage(request: StorageRequest): Promise; + abstract postAccountPayment(request: PaymentRequest): Promise; + abstract postAccountLicense(data: FormData): Promise; + abstract postAccountKeys(request: KeysRequest): Promise; + abstract postAccountVerifyEmail(): Promise; + abstract postAccountVerifyEmailToken(request: VerifyEmailRequest): Promise; + abstract postAccountRecoverDelete(request: DeleteRecoverRequest): Promise; + abstract postAccountRecoverDeleteToken(request: VerifyDeleteRecoverRequest): Promise; + abstract postAccountKdf(request: KdfRequest): Promise; + abstract postUserApiKey(id: string, request: SecretVerificationRequest): Promise; + abstract postUserRotateApiKey( + id: string, + request: SecretVerificationRequest, + ): Promise; + abstract postConvertToKeyConnector(): Promise; //passwordless - getAuthRequest: (id: string) => Promise; - putAuthRequest: (id: string, request: PasswordlessAuthRequest) => Promise; - getAuthRequests: () => Promise>; - getLastAuthRequest: () => Promise; + abstract getAuthRequest(id: string): Promise; + abstract putAuthRequest( + id: string, + request: PasswordlessAuthRequest, + ): Promise; + abstract getAuthRequests(): Promise>; + abstract getLastAuthRequest(): Promise; - getUserBillingHistory: () => Promise; - getUserBillingPayment: () => Promise; + abstract getUserBillingHistory(): Promise; + abstract getUserBillingPayment(): Promise; - getCipher: (id: string) => Promise; - getFullCipherDetails: (id: string) => Promise; - getCipherAdmin: (id: string) => Promise; - getAttachmentData: ( + abstract getCipher(id: string): Promise; + abstract getFullCipherDetails(id: string): Promise; + abstract getCipherAdmin(id: string): Promise; + abstract getAttachmentData( cipherId: string, attachmentId: string, emergencyAccessId?: string, - ) => Promise; - getAttachmentDataAdmin: (cipherId: string, attachmentId: string) => Promise; - getCiphersOrganization: (organizationId: string) => Promise>; - postCipher: (request: CipherRequest) => Promise; - postCipherCreate: (request: CipherCreateRequest) => Promise; - postCipherAdmin: (request: CipherCreateRequest) => Promise; - putCipher: (id: string, request: CipherRequest) => Promise; - putPartialCipher: (id: string, request: CipherPartialRequest) => Promise; - putCipherAdmin: (id: string, request: CipherRequest) => Promise; - deleteCipher: (id: string) => Promise; - deleteCipherAdmin: (id: string) => Promise; - deleteManyCiphers: (request: CipherBulkDeleteRequest) => Promise; - deleteManyCiphersAdmin: (request: CipherBulkDeleteRequest) => Promise; - putMoveCiphers: (request: CipherBulkMoveRequest) => Promise; - putShareCipher: (id: string, request: CipherShareRequest) => Promise; - putShareCiphers: (request: CipherBulkShareRequest) => Promise>; - putCipherCollections: ( + ): Promise; + abstract getAttachmentDataAdmin( + cipherId: string, + attachmentId: string, + ): Promise; + abstract getCiphersOrganization(organizationId: string): Promise>; + abstract postCipher(request: CipherRequest): Promise; + abstract postCipherCreate(request: CipherCreateRequest): Promise; + abstract postCipherAdmin(request: CipherCreateRequest): Promise; + abstract putCipher(id: string, request: CipherRequest): Promise; + abstract putPartialCipher(id: string, request: CipherPartialRequest): Promise; + abstract putCipherAdmin(id: string, request: CipherRequest): Promise; + abstract deleteCipher(id: string): Promise; + abstract deleteCipherAdmin(id: string): Promise; + abstract deleteManyCiphers(request: CipherBulkDeleteRequest): Promise; + abstract deleteManyCiphersAdmin(request: CipherBulkDeleteRequest): Promise; + abstract putMoveCiphers(request: CipherBulkMoveRequest): Promise; + abstract putShareCipher(id: string, request: CipherShareRequest): Promise; + abstract putShareCiphers(request: CipherBulkShareRequest): Promise>; + abstract putCipherCollections( id: string, request: CipherCollectionsRequest, - ) => Promise; - putCipherCollectionsAdmin: (id: string, request: CipherCollectionsRequest) => Promise; - postPurgeCiphers: (request: SecretVerificationRequest, organizationId?: string) => Promise; - putDeleteCipher: (id: string) => Promise; - putDeleteCipherAdmin: (id: string) => Promise; - putDeleteManyCiphers: (request: CipherBulkDeleteRequest) => Promise; - putDeleteManyCiphersAdmin: (request: CipherBulkDeleteRequest) => Promise; - putRestoreCipher: (id: string) => Promise; - putRestoreCipherAdmin: (id: string) => Promise; - putRestoreManyCiphers: ( + ): Promise; + abstract putCipherCollectionsAdmin(id: string, request: CipherCollectionsRequest): Promise; + abstract postPurgeCiphers( + request: SecretVerificationRequest, + organizationId?: string, + ): Promise; + abstract putDeleteCipher(id: string): Promise; + abstract putDeleteCipherAdmin(id: string): Promise; + abstract putDeleteManyCiphers(request: CipherBulkDeleteRequest): Promise; + abstract putDeleteManyCiphersAdmin(request: CipherBulkDeleteRequest): Promise; + abstract putRestoreCipher(id: string): Promise; + abstract putRestoreCipherAdmin(id: string): Promise; + abstract putRestoreManyCiphers( request: CipherBulkRestoreRequest, - ) => Promise>; - putRestoreManyCiphersAdmin: ( + ): Promise>; + abstract putRestoreManyCiphersAdmin( request: CipherBulkRestoreRequest, - ) => Promise>; + ): Promise>; - postCipherAttachment: ( + abstract postCipherAttachment( id: string, request: AttachmentRequest, - ) => Promise; - deleteCipherAttachment: (id: string, attachmentId: string) => Promise; - deleteCipherAttachmentAdmin: (id: string, attachmentId: string) => Promise; - postShareCipherAttachment: ( + ): Promise; + abstract deleteCipherAttachment(id: string, attachmentId: string): Promise; + abstract deleteCipherAttachmentAdmin(id: string, attachmentId: string): Promise; + abstract postShareCipherAttachment( id: string, attachmentId: string, data: FormData, organizationId: string, - ) => Promise; - renewAttachmentUploadUrl: ( + ): Promise; + abstract renewAttachmentUploadUrl( id: string, attachmentId: string, - ) => Promise; - postAttachmentFile: (id: string, attachmentId: string, data: FormData) => Promise; + ): Promise; + abstract postAttachmentFile(id: string, attachmentId: string, data: FormData): Promise; - getUserCollections: () => Promise>; - getCollections: (organizationId: string) => Promise>; - getCollectionUsers: (organizationId: string, id: string) => Promise; - getCollectionAccessDetails: ( + abstract getUserCollections(): Promise>; + abstract getCollections(organizationId: string): Promise>; + abstract getCollectionUsers( organizationId: string, id: string, - ) => Promise; - getManyCollectionsWithAccessDetails: ( + ): Promise; + abstract getCollectionAccessDetails( + organizationId: string, + id: string, + ): Promise; + abstract getManyCollectionsWithAccessDetails( orgId: string, - ) => Promise>; - postCollection: ( + ): Promise>; + abstract postCollection( organizationId: string, request: CollectionRequest, - ) => Promise; - putCollection: ( + ): Promise; + abstract putCollection( organizationId: string, id: string, request: CollectionRequest, - ) => Promise; - deleteCollection: (organizationId: string, id: string) => Promise; - deleteManyCollections: (organizationId: string, collectionIds: string[]) => Promise; + ): Promise; + abstract deleteCollection(organizationId: string, id: string): Promise; + abstract deleteManyCollections(organizationId: string, collectionIds: string[]): Promise; - getGroupUsers: (organizationId: string, id: string) => Promise; - deleteGroupUser: (organizationId: string, id: string, organizationUserId: string) => Promise; - - getSync: () => Promise; - - getSettingsDomains: () => Promise; - putSettingsDomains: (request: UpdateDomainsRequest) => Promise; - - getTwoFactorProviders: () => Promise>; - getTwoFactorOrganizationProviders: ( + abstract getGroupUsers(organizationId: string, id: string): Promise; + abstract deleteGroupUser( organizationId: string, - ) => Promise>; - getTwoFactorAuthenticator: ( + id: string, + organizationUserId: string, + ): Promise; + + abstract getSync(): Promise; + + abstract getSettingsDomains(): Promise; + abstract putSettingsDomains(request: UpdateDomainsRequest): Promise; + + abstract getTwoFactorProviders(): Promise>; + abstract getTwoFactorOrganizationProviders( + organizationId: string, + ): Promise>; + abstract getTwoFactorAuthenticator( request: SecretVerificationRequest, - ) => Promise; - getTwoFactorEmail: (request: SecretVerificationRequest) => Promise; - getTwoFactorDuo: (request: SecretVerificationRequest) => Promise; - getTwoFactorOrganizationDuo: ( + ): Promise; + abstract getTwoFactorEmail(request: SecretVerificationRequest): Promise; + abstract getTwoFactorDuo(request: SecretVerificationRequest): Promise; + abstract getTwoFactorOrganizationDuo( organizationId: string, request: SecretVerificationRequest, - ) => Promise; - getTwoFactorYubiKey: (request: SecretVerificationRequest) => Promise; - getTwoFactorWebAuthn: (request: SecretVerificationRequest) => Promise; - getTwoFactorWebAuthnChallenge: (request: SecretVerificationRequest) => Promise; - getTwoFactorRecover: (request: SecretVerificationRequest) => Promise; - putTwoFactorAuthenticator: ( + ): Promise; + abstract getTwoFactorYubiKey( + request: SecretVerificationRequest, + ): Promise; + abstract getTwoFactorWebAuthn( + request: SecretVerificationRequest, + ): Promise; + abstract getTwoFactorWebAuthnChallenge( + request: SecretVerificationRequest, + ): Promise; + abstract getTwoFactorRecover( + request: SecretVerificationRequest, + ): Promise; + abstract putTwoFactorAuthenticator( request: UpdateTwoFactorAuthenticatorRequest, - ) => Promise; - deleteTwoFactorAuthenticator: ( + ): Promise; + abstract deleteTwoFactorAuthenticator( request: DisableTwoFactorAuthenticatorRequest, - ) => Promise; - putTwoFactorEmail: (request: UpdateTwoFactorEmailRequest) => Promise; - putTwoFactorDuo: (request: UpdateTwoFactorDuoRequest) => Promise; - putTwoFactorOrganizationDuo: ( + ): Promise; + abstract putTwoFactorEmail(request: UpdateTwoFactorEmailRequest): Promise; + abstract putTwoFactorDuo(request: UpdateTwoFactorDuoRequest): Promise; + abstract putTwoFactorOrganizationDuo( organizationId: string, request: UpdateTwoFactorDuoRequest, - ) => Promise; - putTwoFactorYubiKey: ( + ): Promise; + abstract putTwoFactorYubiKey( request: UpdateTwoFactorYubikeyOtpRequest, - ) => Promise; - putTwoFactorWebAuthn: ( + ): Promise; + abstract putTwoFactorWebAuthn( request: UpdateTwoFactorWebAuthnRequest, - ) => Promise; - deleteTwoFactorWebAuthn: ( + ): Promise; + abstract deleteTwoFactorWebAuthn( request: UpdateTwoFactorWebAuthnDeleteRequest, - ) => Promise; - putTwoFactorDisable: (request: TwoFactorProviderRequest) => Promise; - putTwoFactorOrganizationDisable: ( + ): Promise; + abstract putTwoFactorDisable( + request: TwoFactorProviderRequest, + ): Promise; + abstract putTwoFactorOrganizationDisable( organizationId: string, request: TwoFactorProviderRequest, - ) => Promise; - postTwoFactorEmailSetup: (request: TwoFactorEmailRequest) => Promise; - postTwoFactorEmail: (request: TwoFactorEmailRequest) => Promise; - getDeviceVerificationSettings: () => Promise; - putDeviceVerificationSettings: ( + ): Promise; + abstract postTwoFactorEmailSetup(request: TwoFactorEmailRequest): Promise; + abstract postTwoFactorEmail(request: TwoFactorEmailRequest): Promise; + abstract getDeviceVerificationSettings(): Promise; + abstract putDeviceVerificationSettings( request: DeviceVerificationRequest, - ) => Promise; + ): Promise; - getCloudCommunicationsEnabled: () => Promise; + abstract getCloudCommunicationsEnabled(): Promise; abstract getOrganizationConnection( id: string, type: OrganizationConnectionType, @@ -340,136 +367,147 @@ export abstract class ApiService { configType: { new (response: any): TConfig }, organizationConnectionId: string, ): Promise>; - deleteOrganizationConnection: (id: string) => Promise; - getPlans: () => Promise>; + abstract deleteOrganizationConnection(id: string): Promise; + abstract getPlans(): Promise>; - getProviderUsers: (providerId: string) => Promise>; - getProviderUser: (providerId: string, id: string) => Promise; - postProviderUserInvite: (providerId: string, request: ProviderUserInviteRequest) => Promise; - postProviderUserReinvite: (providerId: string, id: string) => Promise; - postManyProviderUserReinvite: ( + abstract getProviderUsers( + providerId: string, + ): Promise>; + abstract getProviderUser(providerId: string, id: string): Promise; + abstract postProviderUserInvite( + providerId: string, + request: ProviderUserInviteRequest, + ): Promise; + abstract postProviderUserReinvite(providerId: string, id: string): Promise; + abstract postManyProviderUserReinvite( providerId: string, request: ProviderUserBulkRequest, - ) => Promise>; - postProviderUserAccept: ( + ): Promise>; + abstract postProviderUserAccept( providerId: string, id: string, request: ProviderUserAcceptRequest, - ) => Promise; - postProviderUserConfirm: ( + ): Promise; + abstract postProviderUserConfirm( providerId: string, id: string, request: ProviderUserConfirmRequest, - ) => Promise; - postProviderUsersPublicKey: ( + ): Promise; + abstract postProviderUsersPublicKey( providerId: string, request: ProviderUserBulkRequest, - ) => Promise>; - postProviderUserBulkConfirm: ( + ): Promise>; + abstract postProviderUserBulkConfirm( providerId: string, request: ProviderUserBulkConfirmRequest, - ) => Promise>; - putProviderUser: ( + ): Promise>; + abstract putProviderUser( providerId: string, id: string, request: ProviderUserUpdateRequest, - ) => Promise; - deleteProviderUser: (organizationId: string, id: string) => Promise; - deleteManyProviderUsers: ( + ): Promise; + abstract deleteProviderUser(organizationId: string, id: string): Promise; + abstract deleteManyProviderUsers( providerId: string, request: ProviderUserBulkRequest, - ) => Promise>; - getProviderClients: ( + ): Promise>; + abstract getProviderClients( providerId: string, - ) => Promise>; - postProviderAddOrganization: ( + ): Promise>; + abstract postProviderAddOrganization( providerId: string, request: ProviderAddOrganizationRequest, - ) => Promise; - postProviderCreateOrganization: ( + ): Promise; + abstract postProviderCreateOrganization( providerId: string, request: ProviderOrganizationCreateRequest, - ) => Promise; - deleteProviderOrganization: (providerId: string, organizationId: string) => Promise; + ): Promise; + abstract deleteProviderOrganization(providerId: string, organizationId: string): Promise; - getEvents: (start: string, end: string, token: string) => Promise>; - getEventsCipher: ( + abstract getEvents( + start: string, + end: string, + token: string, + ): Promise>; + abstract getEventsCipher( id: string, start: string, end: string, token: string, - ) => Promise>; - getEventsOrganization: ( + ): Promise>; + abstract getEventsOrganization( id: string, start: string, end: string, token: string, - ) => Promise>; - getEventsOrganizationUser: ( + ): Promise>; + abstract getEventsOrganizationUser( organizationId: string, id: string, start: string, end: string, token: string, - ) => Promise>; - getEventsProvider: ( + ): Promise>; + abstract getEventsProvider( id: string, start: string, end: string, token: string, - ) => Promise>; - getEventsProviderUser: ( + ): Promise>; + abstract getEventsProviderUser( providerId: string, id: string, start: string, end: string, token: string, - ) => Promise>; + ): Promise>; /** * Posts events for a user * @param request The array of events to upload * @param userId The optional user id the events belong to. If no user id is provided the active user id is used. */ - postEventsCollect: (request: EventRequest[], userId?: UserId) => Promise; + abstract postEventsCollect(request: EventRequest[], userId?: UserId): Promise; - deleteSsoUser: (organizationId: string) => Promise; - getSsoUserIdentifier: () => Promise; + abstract deleteSsoUser(organizationId: string): Promise; + abstract getSsoUserIdentifier(): Promise; - getUserPublicKey: (id: string) => Promise; + abstract getUserPublicKey(id: string): Promise; - getHibpBreach: (username: string) => Promise; + abstract getHibpBreach(username: string): Promise; - postBitPayInvoice: (request: BitPayInvoiceRequest) => Promise; - postSetupPayment: () => Promise; + abstract postBitPayInvoice(request: BitPayInvoiceRequest): Promise; + abstract postSetupPayment(): Promise; - getActiveBearerToken: () => Promise; - fetch: (request: Request) => Promise; - nativeFetch: (request: Request) => Promise; + abstract getActiveBearerToken(): Promise; + abstract fetch(request: Request): Promise; + abstract nativeFetch(request: Request): Promise; - preValidateSso: (identifier: string) => Promise; + abstract preValidateSso(identifier: string): Promise; - postCreateSponsorship: ( + abstract postCreateSponsorship( sponsorshipOrgId: string, request: OrganizationSponsorshipCreateRequest, - ) => Promise; - getSponsorshipSyncStatus: ( + ): Promise; + abstract getSponsorshipSyncStatus( sponsoredOrgId: string, - ) => Promise; - deleteRemoveSponsorship: (sponsoringOrgId: string) => Promise; - postPreValidateSponsorshipToken: ( + ): Promise; + abstract deleteRemoveSponsorship(sponsoringOrgId: string): Promise; + abstract postPreValidateSponsorshipToken( sponsorshipToken: string, - ) => Promise; - postRedeemSponsorship: ( + ): Promise; + abstract postRedeemSponsorship( sponsorshipToken: string, request: OrganizationSponsorshipRedeemRequest, - ) => Promise; + ): Promise; - getMasterKeyFromKeyConnector: (keyConnectorUrl: string) => Promise; - postUserKeyToKeyConnector: ( + abstract getMasterKeyFromKeyConnector( + keyConnectorUrl: string, + ): Promise; + abstract postUserKeyToKeyConnector( keyConnectorUrl: string, request: KeyConnectorUserKeyRequest, - ) => Promise; - getKeyConnectorAlive: (keyConnectorUrl: string) => Promise; - getOrganizationExport: (organizationId: string) => Promise; + ): Promise; + abstract getKeyConnectorAlive(keyConnectorUrl: string): Promise; + abstract getOrganizationExport(organizationId: string): Promise; } diff --git a/libs/common/src/abstractions/event/event-collection.service.ts b/libs/common/src/abstractions/event/event-collection.service.ts index 6ca94d93a62..4f06b76c5eb 100644 --- a/libs/common/src/abstractions/event/event-collection.service.ts +++ b/libs/common/src/abstractions/event/event-collection.service.ts @@ -1,18 +1,16 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { EventType } from "../../enums"; import { CipherView } from "../../vault/models/view/cipher.view"; export abstract class EventCollectionService { - collectMany: ( + abstract collectMany( eventType: EventType, ciphers: CipherView[], uploadImmediately?: boolean, - ) => Promise; - collect: ( + ): Promise; + abstract collect( eventType: EventType, cipherId?: string, uploadImmediately?: boolean, organizationId?: string, - ) => Promise; + ): Promise; } diff --git a/libs/common/src/abstractions/event/event-upload.service.ts b/libs/common/src/abstractions/event/event-upload.service.ts index af2e7a77e7f..352c7cb0255 100644 --- a/libs/common/src/abstractions/event/event-upload.service.ts +++ b/libs/common/src/abstractions/event/event-upload.service.ts @@ -1,7 +1,5 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { UserId } from "../../types/guid"; export abstract class EventUploadService { - uploadEvents: (userId?: UserId) => Promise; + abstract uploadEvents(userId?: UserId): Promise; } diff --git a/libs/common/src/admin-console/abstractions/organization-domain/org-domain-api.service.abstraction.ts b/libs/common/src/admin-console/abstractions/organization-domain/org-domain-api.service.abstraction.ts index 5a393ed1996..b1452c1359b 100644 --- a/libs/common/src/admin-console/abstractions/organization-domain/org-domain-api.service.abstraction.ts +++ b/libs/common/src/admin-console/abstractions/organization-domain/org-domain-api.service.abstraction.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { ListResponse } from "../../../models/response/list.response"; import { OrganizationDomainRequest } from "../../services/organization-domain/requests/organization-domain.request"; @@ -8,19 +6,19 @@ import { OrganizationDomainResponse } from "./responses/organization-domain.resp import { VerifiedOrganizationDomainSsoDetailsResponse } from "./responses/verified-organization-domain-sso-details.response"; export abstract class OrgDomainApiServiceAbstraction { - getAllByOrgId: (orgId: string) => Promise>; - getByOrgIdAndOrgDomainId: ( + abstract getAllByOrgId(orgId: string): Promise>; + abstract getByOrgIdAndOrgDomainId( orgId: string, orgDomainId: string, - ) => Promise; - post: ( + ): Promise; + abstract post( orgId: string, orgDomain: OrganizationDomainRequest, - ) => Promise; - verify: (orgId: string, orgDomainId: string) => Promise; - delete: (orgId: string, orgDomainId: string) => Promise; - getClaimedOrgDomainByEmail: (email: string) => Promise; - getVerifiedOrgDomainsByEmail: ( + ): Promise; + abstract verify(orgId: string, orgDomainId: string): Promise; + abstract delete(orgId: string, orgDomainId: string): Promise; + abstract getClaimedOrgDomainByEmail(email: string): Promise; + abstract getVerifiedOrgDomainsByEmail( email: string, - ) => Promise>; + ): Promise>; } diff --git a/libs/common/src/admin-console/abstractions/organization-domain/org-domain.service.abstraction.ts b/libs/common/src/admin-console/abstractions/organization-domain/org-domain.service.abstraction.ts index 05a0b6d722f..7f08d226d15 100644 --- a/libs/common/src/admin-console/abstractions/organization-domain/org-domain.service.abstraction.ts +++ b/libs/common/src/admin-console/abstractions/organization-domain/org-domain.service.abstraction.ts @@ -1,22 +1,20 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Observable } from "rxjs"; import { OrganizationDomainResponse } from "./responses/organization-domain.response"; export abstract class OrgDomainServiceAbstraction { - orgDomains$: Observable; + abstract orgDomains$: Observable; - get: (orgDomainId: string) => OrganizationDomainResponse; + abstract get(orgDomainId: string): OrganizationDomainResponse; - copyDnsTxt: (dnsTxt: string) => void; + abstract copyDnsTxt(dnsTxt: string): void; } // Note: this separate class is designed to hold methods that are not // meant to be used in components (e.g., data write methods) export abstract class OrgDomainInternalServiceAbstraction extends OrgDomainServiceAbstraction { - upsert: (orgDomains: OrganizationDomainResponse[]) => void; - replace: (orgDomains: OrganizationDomainResponse[]) => void; - clearCache: () => void; - delete: (orgDomainIds: string[]) => void; + abstract upsert(orgDomains: OrganizationDomainResponse[]): void; + abstract replace(orgDomains: OrganizationDomainResponse[]): void; + abstract clearCache(): void; + abstract delete(orgDomainIds: string[]): void; } diff --git a/libs/common/src/admin-console/abstractions/organization/organization-api.service.abstraction.ts b/libs/common/src/admin-console/abstractions/organization/organization-api.service.abstraction.ts index 000d1655416..10626d6758f 100644 --- a/libs/common/src/admin-console/abstractions/organization/organization-api.service.abstraction.ts +++ b/libs/common/src/admin-console/abstractions/organization/organization-api.service.abstraction.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { OrganizationApiKeyRequest } from "../../../admin-console/models/request/organization-api-key.request"; import { OrganizationSsoRequest } from "../../../auth/models/request/organization-sso.request"; import { SecretVerificationRequest } from "../../../auth/models/request/secret-verification.request"; @@ -34,60 +32,66 @@ import { OrganizationKeysResponse } from "../../models/response/organization-key import { OrganizationResponse } from "../../models/response/organization.response"; import { ProfileOrganizationResponse } from "../../models/response/profile-organization.response"; -export class OrganizationApiServiceAbstraction { - get: (id: string) => Promise; - getBilling: (id: string) => Promise; - getBillingHistory: (id: string) => Promise; - getSubscription: (id: string) => Promise; - getLicense: (id: string, installationId: string) => Promise; - getAutoEnrollStatus: (identifier: string) => Promise; - create: (request: OrganizationCreateRequest) => Promise; - createWithoutPayment: ( +export abstract class OrganizationApiServiceAbstraction { + abstract get(id: string): Promise; + abstract getBilling(id: string): Promise; + abstract getBillingHistory(id: string): Promise; + abstract getSubscription(id: string): Promise; + abstract getLicense(id: string, installationId: string): Promise; + abstract getAutoEnrollStatus(identifier: string): Promise; + abstract create(request: OrganizationCreateRequest): Promise; + abstract createWithoutPayment( request: OrganizationNoPaymentMethodCreateRequest, - ) => Promise; - createLicense: (data: FormData) => Promise; - save: (id: string, request: OrganizationUpdateRequest) => Promise; - updatePayment: (id: string, request: PaymentRequest) => Promise; - upgrade: (id: string, request: OrganizationUpgradeRequest) => Promise; - updatePasswordManagerSeats: ( + ): Promise; + abstract createLicense(data: FormData): Promise; + abstract save(id: string, request: OrganizationUpdateRequest): Promise; + abstract updatePayment(id: string, request: PaymentRequest): Promise; + abstract upgrade(id: string, request: OrganizationUpgradeRequest): Promise; + abstract updatePasswordManagerSeats( id: string, request: OrganizationSubscriptionUpdateRequest, - ) => Promise; - updateSecretsManagerSubscription: ( + ): Promise; + abstract updateSecretsManagerSubscription( id: string, request: OrganizationSmSubscriptionUpdateRequest, - ) => Promise; - updateSeats: (id: string, request: SeatRequest) => Promise; - updateStorage: (id: string, request: StorageRequest) => Promise; - verifyBank: (id: string, request: VerifyBankRequest) => Promise; - reinstate: (id: string) => Promise; - leave: (id: string) => Promise; - delete: (id: string, request: SecretVerificationRequest) => Promise; - deleteUsingToken: ( + ): Promise; + abstract updateSeats(id: string, request: SeatRequest): Promise; + abstract updateStorage(id: string, request: StorageRequest): Promise; + abstract verifyBank(id: string, request: VerifyBankRequest): Promise; + abstract reinstate(id: string): Promise; + abstract leave(id: string): Promise; + abstract delete(id: string, request: SecretVerificationRequest): Promise; + abstract deleteUsingToken( organizationId: string, request: OrganizationVerifyDeleteRecoverRequest, - ) => Promise; - updateLicense: (id: string, data: FormData) => Promise; - importDirectory: (organizationId: string, request: ImportDirectoryRequest) => Promise; - getOrCreateApiKey: (id: string, request: OrganizationApiKeyRequest) => Promise; - getApiKeyInformation: ( + ): Promise; + abstract updateLicense(id: string, data: FormData): Promise; + abstract importDirectory(organizationId: string, request: ImportDirectoryRequest): Promise; + abstract getOrCreateApiKey( + id: string, + request: OrganizationApiKeyRequest, + ): Promise; + abstract getApiKeyInformation( id: string, organizationApiKeyType?: OrganizationApiKeyType, - ) => Promise>; - rotateApiKey: (id: string, request: OrganizationApiKeyRequest) => Promise; - getTaxInfo: (id: string) => Promise; - updateTaxInfo: (id: string, request: ExpandedTaxInfoUpdateRequest) => Promise; - getKeys: (id: string) => Promise; - updateKeys: (id: string, request: OrganizationKeysRequest) => Promise; - getSso: (id: string) => Promise; - updateSso: (id: string, request: OrganizationSsoRequest) => Promise; - selfHostedSyncLicense: (id: string) => Promise; - subscribeToSecretsManager: ( + ): Promise>; + abstract rotateApiKey(id: string, request: OrganizationApiKeyRequest): Promise; + abstract getTaxInfo(id: string): Promise; + abstract updateTaxInfo(id: string, request: ExpandedTaxInfoUpdateRequest): Promise; + abstract getKeys(id: string): Promise; + abstract updateKeys( + id: string, + request: OrganizationKeysRequest, + ): Promise; + abstract getSso(id: string): Promise; + abstract updateSso(id: string, request: OrganizationSsoRequest): Promise; + abstract selfHostedSyncLicense(id: string): Promise; + abstract subscribeToSecretsManager( id: string, request: SecretsManagerSubscribeRequest, - ) => Promise; - updateCollectionManagement: ( + ): Promise; + abstract updateCollectionManagement( id: string, request: OrganizationCollectionManagementUpdateRequest, - ) => Promise; + ): Promise; } diff --git a/libs/common/src/admin-console/abstractions/organization/organization.service.abstraction.ts b/libs/common/src/admin-console/abstractions/organization/organization.service.abstraction.ts index 05c214ece13..770cfd0011d 100644 --- a/libs/common/src/admin-console/abstractions/organization/organization.service.abstraction.ts +++ b/libs/common/src/admin-console/abstractions/organization/organization.service.abstraction.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { map, Observable } from "rxjs"; import { UserId } from "../../../types/guid"; @@ -68,20 +66,20 @@ export abstract class OrganizationService { * Publishes state for all organizations under the specified user. * @returns An observable list of organizations */ - organizations$: (userId: UserId) => Observable; + abstract organizations$(userId: UserId): Observable; // @todo Clean these up. Continuing to expand them is not recommended. // @see https://bitwarden.atlassian.net/browse/AC-2252 - memberOrganizations$: (userId: UserId) => Observable; + abstract memberOrganizations$(userId: UserId): Observable; /** * Emits true if the user can create or manage a Free Bitwarden Families sponsorship. */ - canManageSponsorships$: (userId: UserId) => Observable; + abstract canManageSponsorships$(userId: UserId): Observable; /** * Emits true if any of the user's organizations have a Free Bitwarden Families sponsorship available. */ - familySponsorshipAvailable$: (userId: UserId) => Observable; - hasOrganizations: (userId: UserId) => Observable; + abstract familySponsorshipAvailable$(userId: UserId): Observable; + abstract hasOrganizations(userId: UserId): Observable; } /** @@ -96,7 +94,7 @@ export abstract class InternalOrganizationServiceAbstraction extends Organizatio * @param organization The organization state being saved. * @param userId The userId to replace state for. */ - upsert: (OrganizationData: OrganizationData, userId: UserId) => Promise; + abstract upsert(OrganizationData: OrganizationData, userId: UserId): Promise; /** * Replaces state for the entire registered organization list for the specified user. @@ -107,5 +105,8 @@ export abstract class InternalOrganizationServiceAbstraction extends Organizatio * user. * @param userId The userId to replace state for. */ - replace: (organizations: { [id: string]: OrganizationData }, userId: UserId) => Promise; + abstract replace( + organizations: { [id: string]: OrganizationData }, + userId: UserId, + ): Promise; } diff --git a/libs/common/src/admin-console/abstractions/provider.service.ts b/libs/common/src/admin-console/abstractions/provider.service.ts index 0cd21174ea1..340156020ff 100644 --- a/libs/common/src/admin-console/abstractions/provider.service.ts +++ b/libs/common/src/admin-console/abstractions/provider.service.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Observable } from "rxjs"; import { UserId } from "../../types/guid"; @@ -7,8 +5,8 @@ import { ProviderData } from "../models/data/provider.data"; import { Provider } from "../models/domain/provider"; export abstract class ProviderService { - get$: (id: string) => Observable; - get: (id: string) => Promise; - getAll: () => Promise; - save: (providers: { [id: string]: ProviderData }, userId?: UserId) => Promise; + abstract get$(id: string): Observable; + abstract get(id: string): Promise; + abstract getAll(): Promise; + abstract save(providers: { [id: string]: ProviderData }, userId?: UserId): Promise; } diff --git a/libs/common/src/admin-console/abstractions/provider/provider-api.service.abstraction.ts b/libs/common/src/admin-console/abstractions/provider/provider-api.service.abstraction.ts index ffe79f0ad3b..f998fdc8ab7 100644 --- a/libs/common/src/admin-console/abstractions/provider/provider-api.service.abstraction.ts +++ b/libs/common/src/admin-console/abstractions/provider/provider-api.service.abstraction.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { AddableOrganizationResponse } from "@bitwarden/common/admin-console/models/response/addable-organization.response"; import { ProviderSetupRequest } from "../../models/request/provider/provider-setup.request"; @@ -7,21 +5,23 @@ import { ProviderUpdateRequest } from "../../models/request/provider/provider-up import { ProviderVerifyRecoverDeleteRequest } from "../../models/request/provider/provider-verify-recover-delete.request"; import { ProviderResponse } from "../../models/response/provider/provider.response"; -export class ProviderApiServiceAbstraction { - postProviderSetup: (id: string, request: ProviderSetupRequest) => Promise; - getProvider: (id: string) => Promise; - putProvider: (id: string, request: ProviderUpdateRequest) => Promise; - providerRecoverDeleteToken: ( +export abstract class ProviderApiServiceAbstraction { + abstract postProviderSetup(id: string, request: ProviderSetupRequest): Promise; + abstract getProvider(id: string): Promise; + abstract putProvider(id: string, request: ProviderUpdateRequest): Promise; + abstract providerRecoverDeleteToken( organizationId: string, request: ProviderVerifyRecoverDeleteRequest, - ) => Promise; - deleteProvider: (id: string) => Promise; - getProviderAddableOrganizations: (providerId: string) => Promise; - addOrganizationToProvider: ( + ): Promise; + abstract deleteProvider(id: string): Promise; + abstract getProviderAddableOrganizations( + providerId: string, + ): Promise; + abstract addOrganizationToProvider( providerId: string, request: { key: string; organizationId: string; }, - ) => Promise; + ): Promise; } diff --git a/libs/common/src/auth/abstractions/account.service.ts b/libs/common/src/auth/abstractions/account.service.ts index 1686eefda06..a3dabeecf8a 100644 --- a/libs/common/src/auth/abstractions/account.service.ts +++ b/libs/common/src/auth/abstractions/account.service.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Observable } from "rxjs"; import { UserId } from "../../types/guid"; @@ -35,20 +33,20 @@ export function accountInfoEqual(a: AccountInfo, b: AccountInfo) { } export abstract class AccountService { - accounts$: Observable>; + abstract accounts$: Observable>; - activeAccount$: Observable; + abstract activeAccount$: Observable; /** * Observable of the last activity time for each account. */ - accountActivity$: Observable>; + abstract accountActivity$: Observable>; /** Observable of the new device login verification property for the account. */ - accountVerifyNewDeviceLogin$: Observable; + abstract accountVerifyNewDeviceLogin$: Observable; /** Account list in order of descending recency */ - sortedUserIds$: Observable; + abstract sortedUserIds$: Observable; /** Next account that is not the current active account */ - nextUpAccount$: Observable; + abstract nextUpAccount$: Observable; /** * Updates the `accounts$` observable with the new account data. * diff --git a/libs/common/src/auth/abstractions/anonymous-hub.service.ts b/libs/common/src/auth/abstractions/anonymous-hub.service.ts index 8e705d67bfe..624a3a04d53 100644 --- a/libs/common/src/auth/abstractions/anonymous-hub.service.ts +++ b/libs/common/src/auth/abstractions/anonymous-hub.service.ts @@ -1,6 +1,4 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore export abstract class AnonymousHubService { - createHubConnection: (token: string) => Promise; - stopHubConnection: () => Promise; + abstract createHubConnection(token: string): Promise; + abstract stopHubConnection(): Promise; } diff --git a/libs/common/src/auth/abstractions/avatar.service.ts b/libs/common/src/auth/abstractions/avatar.service.ts index 89729aa3712..bd2c382e610 100644 --- a/libs/common/src/auth/abstractions/avatar.service.ts +++ b/libs/common/src/auth/abstractions/avatar.service.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Observable } from "rxjs"; import { UserId } from "../../types/guid"; @@ -9,7 +7,7 @@ export abstract class AvatarService { * An observable monitoring the active user's avatar color. * The observable updates when the avatar color changes. */ - avatarColor$: Observable; + abstract avatarColor$: Observable; /** * Sets the avatar color of the active user * diff --git a/libs/common/src/auth/abstractions/devices-api.service.abstraction.ts b/libs/common/src/auth/abstractions/devices-api.service.abstraction.ts index cf6cdaefd85..54971a443b7 100644 --- a/libs/common/src/auth/abstractions/devices-api.service.abstraction.ts +++ b/libs/common/src/auth/abstractions/devices-api.service.abstraction.ts @@ -1,47 +1,45 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { ListResponse } from "../../models/response/list.response"; import { DeviceResponse } from "../abstractions/devices/responses/device.response"; import { UpdateDevicesTrustRequest } from "../models/request/update-devices-trust.request"; import { ProtectedDeviceResponse } from "../models/response/protected-device.response"; export abstract class DevicesApiServiceAbstraction { - getKnownDevice: (email: string, deviceIdentifier: string) => Promise; + abstract getKnownDevice(email: string, deviceIdentifier: string): Promise; - getDeviceByIdentifier: (deviceIdentifier: string) => Promise; + abstract getDeviceByIdentifier(deviceIdentifier: string): Promise; - getDevices: () => Promise>; + abstract getDevices(): Promise>; - updateTrustedDeviceKeys: ( + abstract updateTrustedDeviceKeys( deviceIdentifier: string, devicePublicKeyEncryptedUserKey: string, userKeyEncryptedDevicePublicKey: string, deviceKeyEncryptedDevicePrivateKey: string, - ) => Promise; + ): Promise; - updateTrust: ( + abstract updateTrust( updateDevicesTrustRequestModel: UpdateDevicesTrustRequest, deviceIdentifier: string, - ) => Promise; + ): Promise; - getDeviceKeys: (deviceIdentifier: string) => Promise; + abstract getDeviceKeys(deviceIdentifier: string): Promise; /** * Notifies the server that the device has a device key, but didn't receive any associated decryption keys. * Note: For debugging purposes only. * @param deviceIdentifier - current device identifier */ - postDeviceTrustLoss: (deviceIdentifier: string) => Promise; + abstract postDeviceTrustLoss(deviceIdentifier: string): Promise; /** * Deactivates a device * @param deviceId - The device ID */ - deactivateDevice: (deviceId: string) => Promise; + abstract deactivateDevice(deviceId: string): Promise; /** * Removes trust from a list of devices * @param deviceIds - The device IDs to be untrusted */ - untrustDevices: (deviceIds: string[]) => Promise; + abstract untrustDevices(deviceIds: string[]): Promise; } diff --git a/libs/common/src/auth/abstractions/token.service.ts b/libs/common/src/auth/abstractions/token.service.ts index 0c8db6fdcd1..2139f32fca2 100644 --- a/libs/common/src/auth/abstractions/token.service.ts +++ b/libs/common/src/auth/abstractions/token.service.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Observable } from "rxjs"; import { VaultTimeout, VaultTimeoutAction } from "../../key-management/vault-timeout"; @@ -27,20 +25,20 @@ export abstract class TokenService { * * @returns A promise that resolves with the SetTokensResult containing the tokens that were set. */ - setTokens: ( + abstract setTokens( accessToken: string, vaultTimeoutAction: VaultTimeoutAction, vaultTimeout: VaultTimeout, refreshToken?: string, clientIdClientSecret?: [string, string], - ) => Promise; + ): Promise; /** * Clears the access token, refresh token, API Key Client ID, and API Key Client Secret out of memory, disk, and secure storage if supported. * @param userId The optional user id to clear the tokens for; if not provided, the active user id is used. * @returns A promise that resolves when the tokens have been cleared. */ - clearTokens: (userId?: UserId) => Promise; + abstract clearTokens(userId?: UserId): Promise; /** * Sets the access token in memory or disk based on the given vaultTimeoutAction and vaultTimeout @@ -51,11 +49,11 @@ export abstract class TokenService { * @param vaultTimeout The timeout for the vault. * @returns A promise that resolves with the access token that has been set. */ - setAccessToken: ( + abstract setAccessToken( accessToken: string, vaultTimeoutAction: VaultTimeoutAction, vaultTimeout: VaultTimeout, - ) => Promise; + ): Promise; // TODO: revisit having this public clear method approach once the state service is fully deprecated. /** @@ -67,21 +65,21 @@ export abstract class TokenService { * pass in the vaultTimeoutAction and vaultTimeout. * This avoids a circular dependency between the StateService, TokenService, and VaultTimeoutSettingsService. */ - clearAccessToken: (userId?: UserId) => Promise; + abstract clearAccessToken(userId?: UserId): Promise; /** * Gets the access token * @param userId - The optional user id to get the access token for; if not provided, the active user is used. * @returns A promise that resolves with the access token or null. */ - getAccessToken: (userId?: UserId) => Promise; + abstract getAccessToken(userId?: UserId): Promise; /** * Gets the refresh token. * @param userId - The optional user id to get the refresh token for; if not provided, the active user is used. * @returns A promise that resolves with the refresh token or null. */ - getRefreshToken: (userId?: UserId) => Promise; + abstract getRefreshToken(userId?: UserId): Promise; /** * Sets the API Key Client ID for the active user id in memory or disk based on the given vaultTimeoutAction and vaultTimeout. @@ -90,18 +88,18 @@ export abstract class TokenService { * @param vaultTimeout The timeout for the vault. * @returns A promise that resolves with the API Key Client ID that has been set. */ - setClientId: ( + abstract setClientId( clientId: string, vaultTimeoutAction: VaultTimeoutAction, vaultTimeout: VaultTimeout, userId?: UserId, - ) => Promise; + ): Promise; /** * Gets the API Key Client ID for the active user. * @returns A promise that resolves with the API Key Client ID or undefined */ - getClientId: (userId?: UserId) => Promise; + abstract getClientId(userId?: UserId): Promise; /** * Sets the API Key Client Secret for the active user id in memory or disk based on the given vaultTimeoutAction and vaultTimeout. @@ -110,18 +108,18 @@ export abstract class TokenService { * @param vaultTimeout The timeout for the vault. * @returns A promise that resolves with the client secret that has been set. */ - setClientSecret: ( + abstract setClientSecret( clientSecret: string, vaultTimeoutAction: VaultTimeoutAction, vaultTimeout: VaultTimeout, userId?: UserId, - ) => Promise; + ): Promise; /** * Gets the API Key Client Secret for the active user. * @returns A promise that resolves with the API Key Client Secret or undefined */ - getClientSecret: (userId?: UserId) => Promise; + abstract getClientSecret(userId?: UserId): Promise; /** * Sets the two factor token for the given email in global state. @@ -131,21 +129,21 @@ export abstract class TokenService { * @param twoFactorToken The two factor token to set. * @returns A promise that resolves when the two factor token has been set. */ - setTwoFactorToken: (email: string, twoFactorToken: string) => Promise; + abstract setTwoFactorToken(email: string, twoFactorToken: string): Promise; /** * Gets the two factor token for the given email. * @param email The email to get the two factor token for. * @returns A promise that resolves with the two factor token for the given email or null if it isn't found. */ - getTwoFactorToken: (email: string) => Promise; + abstract getTwoFactorToken(email: string): Promise; /** * Clears the two factor token for the given email out of global state. * @param email The email to clear the two factor token for. * @returns A promise that resolves when the two factor token has been cleared. */ - clearTwoFactorToken: (email: string) => Promise; + abstract clearTwoFactorToken(email: string): Promise; /** * Decodes the access token. @@ -153,13 +151,13 @@ export abstract class TokenService { * If null, the currently active user's token is used. * @returns A promise that resolves with the decoded access token. */ - decodeAccessToken: (tokenOrUserId?: string | UserId) => Promise; + abstract decodeAccessToken(tokenOrUserId?: string | UserId): Promise; /** * Gets the expiration date for the access token. Returns if token can't be decoded or has no expiration * @returns A promise that resolves with the expiration date for the access token. */ - getTokenExpirationDate: () => Promise; + abstract getTokenExpirationDate(): Promise; /** * Calculates the adjusted time in seconds until the access token expires, considering an optional offset. @@ -170,58 +168,58 @@ export abstract class TokenService { * based on the actual expiration. * @returns {Promise} Promise resolving to the adjusted seconds remaining. */ - tokenSecondsRemaining: (offsetSeconds?: number) => Promise; + abstract tokenSecondsRemaining(offsetSeconds?: number): Promise; /** * Checks if the access token needs to be refreshed. * @param {number} [minutes=5] - Optional number of minutes before the access token expires to consider refreshing it. * @returns A promise that resolves with a boolean indicating if the access token needs to be refreshed. */ - tokenNeedsRefresh: (minutes?: number) => Promise; + abstract tokenNeedsRefresh(minutes?: number): Promise; /** * Gets the user id for the active user from the access token. * @returns A promise that resolves with the user id for the active user. * @deprecated Use AccountService.activeAccount$ instead. */ - getUserId: () => Promise; + abstract getUserId(): Promise; /** * Gets the email for the active user from the access token. * @returns A promise that resolves with the email for the active user. * @deprecated Use AccountService.activeAccount$ instead. */ - getEmail: () => Promise; + abstract getEmail(): Promise; /** * Gets the email verified status for the active user from the access token. * @returns A promise that resolves with the email verified status for the active user. */ - getEmailVerified: () => Promise; + abstract getEmailVerified(): Promise; /** * Gets the name for the active user from the access token. * @returns A promise that resolves with the name for the active user. * @deprecated Use AccountService.activeAccount$ instead. */ - getName: () => Promise; + abstract getName(): Promise; /** * Gets the issuer for the active user from the access token. * @returns A promise that resolves with the issuer for the active user. */ - getIssuer: () => Promise; + abstract getIssuer(): Promise; /** * Gets whether or not the user authenticated via an external mechanism. * @param userId The optional user id to check for external authN status; if not provided, the active user is used. * @returns A promise that resolves with a boolean representing the user's external authN status. */ - getIsExternal: (userId: UserId) => Promise; + abstract getIsExternal(userId: UserId): Promise; /** Gets the active or passed in user's security stamp */ - getSecurityStamp: (userId?: UserId) => Promise; + abstract getSecurityStamp(userId?: UserId): Promise; /** Sets the security stamp for the active or passed in user */ - setSecurityStamp: (securityStamp: string, userId?: UserId) => Promise; + abstract setSecurityStamp(securityStamp: string, userId?: UserId): Promise; } diff --git a/libs/common/src/auth/abstractions/user-verification/user-verification-api.service.abstraction.ts b/libs/common/src/auth/abstractions/user-verification/user-verification-api.service.abstraction.ts index 42abc794061..275df417df2 100644 --- a/libs/common/src/auth/abstractions/user-verification/user-verification-api.service.abstraction.ts +++ b/libs/common/src/auth/abstractions/user-verification/user-verification-api.service.abstraction.ts @@ -1,13 +1,11 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { SecretVerificationRequest } from "../../models/request/secret-verification.request"; import { VerifyOTPRequest } from "../../models/request/verify-otp.request"; import { MasterPasswordPolicyResponse } from "../../models/response/master-password-policy.response"; export abstract class UserVerificationApiServiceAbstraction { - postAccountVerifyOTP: (request: VerifyOTPRequest) => Promise; - postAccountRequestOTP: () => Promise; - postAccountVerifyPassword: ( + abstract postAccountVerifyOTP(request: VerifyOTPRequest): Promise; + abstract postAccountRequestOTP(): Promise; + abstract postAccountVerifyPassword( request: SecretVerificationRequest, - ) => Promise; + ): Promise; } diff --git a/libs/common/src/auth/abstractions/user-verification/user-verification.service.abstraction.ts b/libs/common/src/auth/abstractions/user-verification/user-verification.service.abstraction.ts index 2d39854f8d9..d9749d9467c 100644 --- a/libs/common/src/auth/abstractions/user-verification/user-verification.service.abstraction.ts +++ b/libs/common/src/auth/abstractions/user-verification/user-verification.service.abstraction.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { UserId } from "../../../types/guid"; import { SecretVerificationRequest } from "../../models/request/secret-verification.request"; import { UserVerificationOptions } from "../../types/user-verification-options"; @@ -16,9 +14,9 @@ export abstract class UserVerificationService { * @param verificationType Type of verification to restrict the options to * @returns Available verification options for the user */ - getAvailableVerificationOptions: ( + abstract getAvailableVerificationOptions( verificationType: keyof UserVerificationOptions, - ) => Promise; + ): Promise; /** * Create a new request model to be used for server-side verification * @param verification User-supplied verification data (Master Password or OTP) @@ -26,11 +24,11 @@ export abstract class UserVerificationService { * @param alreadyHashed Whether the master password is already hashed * @throws Error if the verification data is invalid */ - buildRequest: ( + abstract buildRequest( verification: Verification, requestClass?: new () => T, alreadyHashed?: boolean, - ) => Promise; + ): Promise; /** * Verifies the user using the provided verification data. * PIN or biometrics are verified client-side. @@ -39,11 +37,11 @@ export abstract class UserVerificationService { * @param verification User-supplied verification data (OTP, MP, PIN, or biometrics) * @throws Error if the verification data is invalid or the verification fails */ - verifyUser: (verification: Verification) => Promise; + abstract verifyUser(verification: Verification): Promise; /** * Request a one-time password (OTP) to be sent to the user's email */ - requestOTP: () => Promise; + abstract requestOTP(): Promise; /** * Check if user has master password or can only use passwordless technologies to log in * Note: This only checks the server, not the local state @@ -51,13 +49,13 @@ export abstract class UserVerificationService { * @returns True if the user has a master password * @deprecated Use UserDecryptionOptionsService.hasMasterPassword$ instead */ - hasMasterPassword: (userId?: string) => Promise; + abstract hasMasterPassword(userId?: string): Promise; /** * Check if the user has a master password and has used it during their current session * @param userId The user id to check. If not provided, the current user id used * @returns True if the user has a master password and has used it in the current session */ - hasMasterPasswordAndMasterKeyHash: (userId?: string) => Promise; + abstract hasMasterPasswordAndMasterKeyHash(userId?: string): Promise; /** * Verifies the user using the provided master password. * Attempts to verify client-side first, then server-side if necessary. @@ -68,9 +66,9 @@ export abstract class UserVerificationService { * @throws Error if the master password is invalid * @returns An object containing the master key, and master password policy options if verified on server. */ - verifyUserByMasterPassword: ( + abstract verifyUserByMasterPassword( verification: MasterPasswordVerification, userId: UserId, email: string, - ) => Promise; + ): Promise; } diff --git a/libs/common/src/auth/abstractions/webauthn/webauthn-login-api.service.abstraction.ts b/libs/common/src/auth/abstractions/webauthn/webauthn-login-api.service.abstraction.ts index ca87710d22f..1e0fc124755 100644 --- a/libs/common/src/auth/abstractions/webauthn/webauthn-login-api.service.abstraction.ts +++ b/libs/common/src/auth/abstractions/webauthn/webauthn-login-api.service.abstraction.ts @@ -1,7 +1,5 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { CredentialAssertionOptionsResponse } from "../../services/webauthn-login/response/credential-assertion-options.response"; -export class WebAuthnLoginApiServiceAbstraction { - getCredentialAssertionOptions: () => Promise; +export abstract class WebAuthnLoginApiServiceAbstraction { + abstract getCredentialAssertionOptions(): Promise; } diff --git a/libs/common/src/auth/abstractions/webauthn/webauthn-login-prf-key.service.abstraction.ts b/libs/common/src/auth/abstractions/webauthn/webauthn-login-prf-key.service.abstraction.ts index 5de89313ecc..d47b7ccbcef 100644 --- a/libs/common/src/auth/abstractions/webauthn/webauthn-login-prf-key.service.abstraction.ts +++ b/libs/common/src/auth/abstractions/webauthn/webauthn-login-prf-key.service.abstraction.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { PrfKey } from "../../../types/key"; /** @@ -9,11 +7,11 @@ export abstract class WebAuthnLoginPrfKeyServiceAbstraction { /** * Get the salt used to generate the PRF-output used when logging in with WebAuthn. */ - getLoginWithPrfSalt: () => Promise; + abstract getLoginWithPrfSalt(): Promise; /** * Create a symmetric key from the PRF-output by stretching it. * This should be used as `ExternalKey` with `RotateableKeySet`. */ - createSymmetricKeyFromPrf: (prf: ArrayBuffer) => Promise; + abstract createSymmetricKeyFromPrf(prf: ArrayBuffer): Promise; } diff --git a/libs/common/src/auth/abstractions/webauthn/webauthn-login.service.abstraction.ts b/libs/common/src/auth/abstractions/webauthn/webauthn-login.service.abstraction.ts index 8e6ffae27a8..c482b1a214e 100644 --- a/libs/common/src/auth/abstractions/webauthn/webauthn-login.service.abstraction.ts +++ b/libs/common/src/auth/abstractions/webauthn/webauthn-login.service.abstraction.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { AuthResult } from "../../models/domain/auth-result"; import { WebAuthnLoginCredentialAssertionOptionsView } from "../../models/view/webauthn-login/webauthn-login-credential-assertion-options.view"; import { WebAuthnLoginCredentialAssertionView } from "../../models/view/webauthn-login/webauthn-login-credential-assertion.view"; @@ -14,7 +12,7 @@ export abstract class WebAuthnLoginServiceAbstraction { * (whether FIDO2 user verification is required, the relying party id, timeout duration for the process to complete, etc.) * for the authenticator. */ - getCredentialAssertionOptions: () => Promise; + abstract getCredentialAssertionOptions(): Promise; /** * Asserts the credential. This involves user interaction with the authenticator @@ -27,9 +25,9 @@ export abstract class WebAuthnLoginServiceAbstraction { * @returns {WebAuthnLoginCredentialAssertionView} The assertion obtained from the authenticator. * If the assertion is not successfully obtained, it returns undefined. */ - assertCredential: ( + abstract assertCredential( credentialAssertionOptions: WebAuthnLoginCredentialAssertionOptionsView, - ) => Promise; + ): Promise; /** * Logs the user in using the assertion obtained from the authenticator. @@ -39,5 +37,5 @@ export abstract class WebAuthnLoginServiceAbstraction { * @param {WebAuthnLoginCredentialAssertionView} assertion - The assertion obtained from the authenticator * that needs to be validated for login. */ - logIn: (assertion: WebAuthnLoginCredentialAssertionView) => Promise; + abstract logIn(assertion: WebAuthnLoginCredentialAssertionView): Promise; } diff --git a/libs/common/src/billing/abstractions/account/account-billing-api.service.abstraction.ts b/libs/common/src/billing/abstractions/account/account-billing-api.service.abstraction.ts index e0e8b7377c5..0f28e728ea2 100644 --- a/libs/common/src/billing/abstractions/account/account-billing-api.service.abstraction.ts +++ b/libs/common/src/billing/abstractions/account/account-billing-api.service.abstraction.ts @@ -1,11 +1,12 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { BillingInvoiceResponse, BillingTransactionResponse, } from "../../models/response/billing.response"; -export class AccountBillingApiServiceAbstraction { - getBillingInvoices: (status?: string, startAfter?: string) => Promise; - getBillingTransactions: (startAfter?: string) => Promise; +export abstract class AccountBillingApiServiceAbstraction { + abstract getBillingInvoices( + status?: string, + startAfter?: string, + ): Promise; + abstract getBillingTransactions(startAfter?: string): Promise; } diff --git a/libs/common/src/billing/abstractions/account/billing-account-profile-state.service.ts b/libs/common/src/billing/abstractions/account/billing-account-profile-state.service.ts index a4253226880..de9642f9194 100644 --- a/libs/common/src/billing/abstractions/account/billing-account-profile-state.service.ts +++ b/libs/common/src/billing/abstractions/account/billing-account-profile-state.service.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Observable } from "rxjs"; import { UserId } from "../../../types/guid"; diff --git a/libs/common/src/billing/abstractions/billing-api.service.abstraction.ts b/libs/common/src/billing/abstractions/billing-api.service.abstraction.ts index 21089933a59..2f3fe9125db 100644 --- a/libs/common/src/billing/abstractions/billing-api.service.abstraction.ts +++ b/libs/common/src/billing/abstractions/billing-api.service.abstraction.ts @@ -1,6 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore - import { TaxInfoResponse } from "@bitwarden/common/billing/models/response/tax-info.response"; import { OrganizationCreateRequest } from "../../admin-console/models/request/organization-create.request"; @@ -20,78 +17,78 @@ import { PaymentMethodResponse } from "../models/response/payment-method.respons import { ProviderSubscriptionResponse } from "../models/response/provider-subscription-response"; export abstract class BillingApiServiceAbstraction { - cancelOrganizationSubscription: ( + abstract cancelOrganizationSubscription( organizationId: string, request: SubscriptionCancellationRequest, - ) => Promise; + ): Promise; - cancelPremiumUserSubscription: (request: SubscriptionCancellationRequest) => Promise; + abstract cancelPremiumUserSubscription(request: SubscriptionCancellationRequest): Promise; - createProviderClientOrganization: ( + abstract createProviderClientOrganization( providerId: string, request: CreateClientOrganizationRequest, - ) => Promise; + ): Promise; - createSetupIntent: (paymentMethodType: PaymentMethodType) => Promise; + abstract createSetupIntent(paymentMethodType: PaymentMethodType): Promise; - getOrganizationBillingMetadata: ( + abstract getOrganizationBillingMetadata( organizationId: string, - ) => Promise; + ): Promise; - getOrganizationPaymentMethod: (organizationId: string) => Promise; + abstract getOrganizationPaymentMethod(organizationId: string): Promise; - getPlans: () => Promise>; + abstract getPlans(): Promise>; - getProviderClientInvoiceReport: (providerId: string, invoiceId: string) => Promise; + abstract getProviderClientInvoiceReport(providerId: string, invoiceId: string): Promise; - getProviderClientOrganizations: ( + abstract getProviderClientOrganizations( providerId: string, - ) => Promise>; + ): Promise>; - getProviderInvoices: (providerId: string) => Promise; + abstract getProviderInvoices(providerId: string): Promise; - getProviderSubscription: (providerId: string) => Promise; + abstract getProviderSubscription(providerId: string): Promise; - getProviderTaxInformation: (providerId: string) => Promise; + abstract getProviderTaxInformation(providerId: string): Promise; - updateOrganizationPaymentMethod: ( + abstract updateOrganizationPaymentMethod( organizationId: string, request: UpdatePaymentMethodRequest, - ) => Promise; + ): Promise; - updateOrganizationTaxInformation: ( + abstract updateOrganizationTaxInformation( organizationId: string, request: ExpandedTaxInfoUpdateRequest, - ) => Promise; + ): Promise; - updateProviderClientOrganization: ( + abstract updateProviderClientOrganization( providerId: string, organizationId: string, request: UpdateClientOrganizationRequest, - ) => Promise; + ): Promise; - updateProviderPaymentMethod: ( + abstract updateProviderPaymentMethod( providerId: string, request: UpdatePaymentMethodRequest, - ) => Promise; + ): Promise; - updateProviderTaxInformation: ( + abstract updateProviderTaxInformation( providerId: string, request: ExpandedTaxInfoUpdateRequest, - ) => Promise; + ): Promise; - verifyOrganizationBankAccount: ( + abstract verifyOrganizationBankAccount( organizationId: string, request: VerifyBankAccountRequest, - ) => Promise; + ): Promise; - verifyProviderBankAccount: ( + abstract verifyProviderBankAccount( providerId: string, request: VerifyBankAccountRequest, - ) => Promise; + ): Promise; - restartSubscription: ( + abstract restartSubscription( organizationId: string, request: OrganizationCreateRequest, - ) => Promise; + ): Promise; } diff --git a/libs/common/src/billing/abstractions/organization-billing.service.ts b/libs/common/src/billing/abstractions/organization-billing.service.ts index 58c537c99cc..113b55465a7 100644 --- a/libs/common/src/billing/abstractions/organization-billing.service.ts +++ b/libs/common/src/billing/abstractions/organization-billing.service.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Observable } from "rxjs"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; @@ -49,20 +47,22 @@ export type SubscriptionInformation = { }; export abstract class OrganizationBillingServiceAbstraction { - getPaymentSource: (organizationId: string) => Promise; + abstract getPaymentSource(organizationId: string): Promise; - purchaseSubscription: (subscription: SubscriptionInformation) => Promise; - - purchaseSubscriptionNoPaymentMethod: ( + abstract purchaseSubscription( subscription: SubscriptionInformation, - ) => Promise; + ): Promise; - startFree: (subscription: SubscriptionInformation) => Promise; + abstract purchaseSubscriptionNoPaymentMethod( + subscription: SubscriptionInformation, + ): Promise; - restartSubscription: ( + abstract startFree(subscription: SubscriptionInformation): Promise; + + abstract restartSubscription( organizationId: string, subscription: SubscriptionInformation, - ) => Promise; + ): Promise; /** * Determines if breadcrumbing policies is enabled for the organizations meeting certain criteria. diff --git a/libs/common/src/key-management/device-trust/abstractions/device-trust.service.abstraction.ts b/libs/common/src/key-management/device-trust/abstractions/device-trust.service.abstraction.ts index d688c7f366b..2bc99e5e5c2 100644 --- a/libs/common/src/key-management/device-trust/abstractions/device-trust.service.abstraction.ts +++ b/libs/common/src/key-management/device-trust/abstractions/device-trust.service.abstraction.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Observable } from "rxjs"; import { OtherDeviceKeysUpdateRequest } from "@bitwarden/common/auth/models/request/update-devices-trust.request"; @@ -15,51 +13,51 @@ export abstract class DeviceTrustServiceAbstraction { * by Platform * @description Checks if the device trust feature is supported for the active user. */ - supportsDeviceTrust$: Observable; + abstract supportsDeviceTrust$: Observable; /** * Emits when a device has been trusted. This emission is specifically for the purpose of notifying * the consuming component to display a toast informing the user the device has been trusted. */ - deviceTrusted$: Observable; + abstract deviceTrusted$: Observable; /** * @description Checks if the device trust feature is supported for the given user. */ - supportsDeviceTrustByUserId$: (userId: UserId) => Observable; + abstract supportsDeviceTrustByUserId$(userId: UserId): Observable; /** * @description Retrieves the users choice to trust the device which can only happen after decryption * Note: this value should only be used once and then reset */ - getShouldTrustDevice: (userId: UserId) => Promise; - setShouldTrustDevice: (userId: UserId, value: boolean) => Promise; + abstract getShouldTrustDevice(userId: UserId): Promise; + abstract setShouldTrustDevice(userId: UserId, value: boolean): Promise; - trustDeviceIfRequired: (userId: UserId) => Promise; + abstract trustDeviceIfRequired(userId: UserId): Promise; - trustDevice: (userId: UserId) => Promise; + abstract trustDevice(userId: UserId): Promise; /** Retrieves the device key if it exists from state or secure storage if supported for the active user. */ - getDeviceKey: (userId: UserId) => Promise; - decryptUserKeyWithDeviceKey: ( + abstract getDeviceKey(userId: UserId): Promise; + abstract decryptUserKeyWithDeviceKey( userId: UserId, encryptedDevicePrivateKey: EncString, encryptedUserKey: EncString, deviceKey: DeviceKey, - ) => Promise; - rotateDevicesTrust: ( + ): Promise; + abstract rotateDevicesTrust( userId: UserId, newUserKey: UserKey, masterPasswordHash: string, - ) => Promise; + ): Promise; /** * Notifies the server that the device has a device key, but didn't receive any associated decryption keys. * Note: For debugging purposes only. */ - recordDeviceTrustLoss: () => Promise; - getRotatedData: ( + abstract recordDeviceTrustLoss(): Promise; + abstract getRotatedData( oldUserKey: UserKey, newUserKey: UserKey, userId: UserId, - ) => Promise; + ): Promise; } diff --git a/libs/common/src/key-management/vault-timeout/abstractions/vault-timeout-settings.service.ts b/libs/common/src/key-management/vault-timeout/abstractions/vault-timeout-settings.service.ts index 9ff362e4009..bcbf0029199 100644 --- a/libs/common/src/key-management/vault-timeout/abstractions/vault-timeout-settings.service.ts +++ b/libs/common/src/key-management/vault-timeout/abstractions/vault-timeout-settings.service.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Observable } from "rxjs"; import { UserId } from "../../../types/guid"; @@ -13,11 +11,11 @@ export abstract class VaultTimeoutSettingsService { * @param vaultTimeoutAction The vault timeout action * @param userId The user id to set the data for. */ - setVaultTimeoutOptions: ( + abstract setVaultTimeoutOptions( userId: UserId, vaultTimeout: VaultTimeout, vaultTimeoutAction: VaultTimeoutAction, - ) => Promise; + ): Promise; /** * Get the available vault timeout actions for the current user @@ -25,13 +23,13 @@ export abstract class VaultTimeoutSettingsService { * **NOTE:** This observable is not yet connected to the state service, so it will not update when the state changes * @param userId The user id to check. If not provided, the current user is used */ - availableVaultTimeoutActions$: (userId?: string) => Observable; + abstract availableVaultTimeoutActions$(userId?: string): Observable; /** * Evaluates the user's available vault timeout actions and returns a boolean representing * if the user can lock or not */ - canLock: (userId: string) => Promise; + abstract canLock(userId: string): Promise; /** * Gets the vault timeout action for the given user id. The returned value is @@ -41,7 +39,7 @@ export abstract class VaultTimeoutSettingsService { * A new action will be emitted if the current state changes or if the user's policy changes and the new policy affects the action. * @param userId - the user id to get the vault timeout action for */ - getVaultTimeoutActionByUserId$: (userId: string) => Observable; + abstract getVaultTimeoutActionByUserId$(userId: string): Observable; /** * Get the vault timeout for the given user id. The returned value is calculated based on the current state @@ -50,14 +48,14 @@ export abstract class VaultTimeoutSettingsService { * A new timeout will be emitted if the current state changes or if the user's policy changes and the new policy affects the timeout. * @param userId The user id to get the vault timeout for */ - getVaultTimeoutByUserId$: (userId: string) => Observable; + abstract getVaultTimeoutByUserId$(userId: string): Observable; /** * Has the user enabled unlock with Biometric. * @param userId The user id to check. If not provided, the current user is used * @returns boolean true if biometric lock is set */ - isBiometricLockSet: (userId?: string) => Promise; + abstract isBiometricLockSet(userId?: string): Promise; - clear: (userId: UserId) => Promise; + abstract clear(userId: UserId): Promise; } diff --git a/libs/common/src/key-management/vault-timeout/abstractions/vault-timeout.service.ts b/libs/common/src/key-management/vault-timeout/abstractions/vault-timeout.service.ts index cb07c7d193a..1c88a5c51ea 100644 --- a/libs/common/src/key-management/vault-timeout/abstractions/vault-timeout.service.ts +++ b/libs/common/src/key-management/vault-timeout/abstractions/vault-timeout.service.ts @@ -1,7 +1,5 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore export abstract class VaultTimeoutService { - checkVaultTimeout: () => Promise; - lock: (userId?: string) => Promise; - logOut: (userId?: string) => Promise; + abstract checkVaultTimeout(): Promise; + abstract lock(userId?: string): Promise; + abstract logOut(userId?: string): Promise; } diff --git a/libs/common/src/platform/abstractions/fido2/fido2-authenticator.service.abstraction.ts b/libs/common/src/platform/abstractions/fido2/fido2-authenticator.service.abstraction.ts index fd3453198e6..c34c4b835cf 100644 --- a/libs/common/src/platform/abstractions/fido2/fido2-authenticator.service.abstraction.ts +++ b/libs/common/src/platform/abstractions/fido2/fido2-authenticator.service.abstraction.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Fido2CredentialView } from "../../../vault/models/view/fido2-credential.view"; /** @@ -17,11 +15,11 @@ export abstract class Fido2AuthenticatorService { * @param abortController An AbortController that can be used to abort the operation. * @returns A promise that resolves with the new credential and an attestation signature. **/ - makeCredential: ( + abstract makeCredential( params: Fido2AuthenticatorMakeCredentialsParams, window: ParentWindowReference, abortController?: AbortController, - ) => Promise; + ): Promise; /** * Generate an assertion using an existing credential as describe in: @@ -31,11 +29,11 @@ export abstract class Fido2AuthenticatorService { * @param abortController An AbortController that can be used to abort the operation. * @returns A promise that resolves with the asserted credential and an assertion signature. */ - getAssertion: ( + abstract getAssertion( params: Fido2AuthenticatorGetAssertionParams, window: ParentWindowReference, abortController?: AbortController, - ) => Promise; + ): Promise; /** * Discover credentials for a given Relying Party @@ -43,7 +41,7 @@ export abstract class Fido2AuthenticatorService { * @param rpId The Relying Party's ID * @returns A promise that resolves with an array of discoverable credentials */ - silentCredentialDiscovery: (rpId: string) => Promise; + abstract silentCredentialDiscovery(rpId: string): Promise; } // FIXME: update to use a const object instead of a typescript enum diff --git a/libs/common/src/platform/abstractions/fido2/fido2-client.service.abstraction.ts b/libs/common/src/platform/abstractions/fido2/fido2-client.service.abstraction.ts index 55d9cce8049..f1ad26673fd 100644 --- a/libs/common/src/platform/abstractions/fido2/fido2-client.service.abstraction.ts +++ b/libs/common/src/platform/abstractions/fido2/fido2-client.service.abstraction.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore export const UserRequestedFallbackAbortReason = "UserRequestedFallback"; export type UserVerification = "discouraged" | "preferred" | "required"; @@ -16,7 +14,7 @@ export type UserVerification = "discouraged" | "preferred" | "required"; * and for returning the results of the latter operations to the Web Authentication API's callers. */ export abstract class Fido2ClientService { - isFido2FeatureEnabled: (hostname: string, origin: string) => Promise; + abstract isFido2FeatureEnabled(hostname: string, origin: string): Promise; /** * Allows WebAuthn Relying Party scripts to request the creation of a new public key credential source. @@ -26,11 +24,11 @@ export abstract class Fido2ClientService { * @param abortController An AbortController that can be used to abort the operation. * @returns A promise that resolves with the new credential. */ - createCredential: ( + abstract createCredential( params: CreateCredentialParams, window: ParentWindowReference, abortController?: AbortController, - ) => Promise; + ): Promise; /** * Allows WebAuthn Relying Party scripts to discover and use an existing public key credential, with the user’s consent. @@ -41,11 +39,11 @@ export abstract class Fido2ClientService { * @param abortController An AbortController that can be used to abort the operation. * @returns A promise that resolves with the asserted credential. */ - assertCredential: ( + abstract assertCredential( params: AssertCredentialParams, window: ParentWindowReference, abortController?: AbortController, - ) => Promise; + ): Promise; } /** diff --git a/libs/common/src/platform/abstractions/fido2/fido2-user-interface.service.abstraction.ts b/libs/common/src/platform/abstractions/fido2/fido2-user-interface.service.abstraction.ts index 1f871f6c70f..28b199da78f 100644 --- a/libs/common/src/platform/abstractions/fido2/fido2-user-interface.service.abstraction.ts +++ b/libs/common/src/platform/abstractions/fido2/fido2-user-interface.service.abstraction.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore /** * Parameters used to ask the user to confirm the creation of a new credential. */ @@ -69,11 +67,11 @@ export abstract class Fido2UserInterfaceService { * @param fallbackSupported Whether or not the browser natively supports WebAuthn. * @param abortController An abort controller that can be used to cancel/close the session. */ - newSession: ( + abstract newSession( fallbackSupported: boolean, window: ParentWindowReference, abortController?: AbortController, - ) => Promise; + ): Promise; } export abstract class Fido2UserInterfaceSession { @@ -84,9 +82,9 @@ export abstract class Fido2UserInterfaceSession { * @param abortController An abort controller that can be used to cancel/close the session. * @returns The ID of the cipher that contains the credentials the user picked. If not cipher was picked, return cipherId = undefined to to let the authenticator throw the error. */ - pickCredential: ( + abstract pickCredential( params: PickCredentialParams, - ) => Promise<{ cipherId: string; userVerified: boolean }>; + ): Promise<{ cipherId: string; userVerified: boolean }>; /** * Ask the user to confirm the creation of a new credential. @@ -95,30 +93,30 @@ export abstract class Fido2UserInterfaceSession { * @param abortController An abort controller that can be used to cancel/close the session. * @returns The ID of the cipher where the new credential should be saved. */ - confirmNewCredential: ( + abstract confirmNewCredential( params: NewCredentialParams, - ) => Promise<{ cipherId: string; userVerified: boolean }>; + ): Promise<{ cipherId: string; userVerified: boolean }>; /** * Make sure that the vault is unlocked. * This will open a window and ask the user to login or unlock the vault if necessary. */ - ensureUnlockedVault: () => Promise; + abstract ensureUnlockedVault(): Promise; /** * Inform the user that the operation was cancelled because their vault contains excluded credentials. * * @param existingCipherIds The IDs of the excluded credentials. */ - informExcludedCredential: (existingCipherIds: string[]) => Promise; + abstract informExcludedCredential(existingCipherIds: string[]): Promise; /** * Inform the user that the operation was cancelled because their vault does not contain any useable credentials. */ - informCredentialNotFound: (abortController?: AbortController) => Promise; + abstract informCredentialNotFound(abortController?: AbortController): Promise; /** * Close the session, including any windows that may be open. */ - close: () => void; + abstract close(): void; } diff --git a/libs/common/src/platform/abstractions/state.service.ts b/libs/common/src/platform/abstractions/state.service.ts index e4dbe76d7e4..4c1c000284e 100644 --- a/libs/common/src/platform/abstractions/state.service.ts +++ b/libs/common/src/platform/abstractions/state.service.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { BiometricKey } from "../../auth/types/biometric-key"; import { Account } from "../models/domain/account"; import { StorageOptions } from "../models/domain/storage-options"; @@ -19,47 +17,47 @@ export type InitOptions = { }; export abstract class StateService { - addAccount: (account: T) => Promise; - clean: (options?: StorageOptions) => Promise; - init: (initOptions?: InitOptions) => Promise; + abstract addAccount(account: T): Promise; + abstract clean(options?: StorageOptions): Promise; + abstract init(initOptions?: InitOptions): Promise; /** * Gets the user's auto key */ - getUserKeyAutoUnlock: (options?: StorageOptions) => Promise; + abstract getUserKeyAutoUnlock(options?: StorageOptions): Promise; /** * Sets the user's auto key */ - setUserKeyAutoUnlock: (value: string | null, options?: StorageOptions) => Promise; + abstract setUserKeyAutoUnlock(value: string | null, options?: StorageOptions): Promise; /** * Gets the user's biometric key */ - getUserKeyBiometric: (options?: StorageOptions) => Promise; + abstract getUserKeyBiometric(options?: StorageOptions): Promise; /** * Checks if the user has a biometric key available */ - hasUserKeyBiometric: (options?: StorageOptions) => Promise; + abstract hasUserKeyBiometric(options?: StorageOptions): Promise; /** * Sets the user's biometric key */ - setUserKeyBiometric: (value: BiometricKey, options?: StorageOptions) => Promise; + abstract setUserKeyBiometric(value: BiometricKey, options?: StorageOptions): Promise; /** * @deprecated For backwards compatible purposes only, use DesktopAutofillSettingsService */ - setEnableDuckDuckGoBrowserIntegration: ( + abstract setEnableDuckDuckGoBrowserIntegration( value: boolean, options?: StorageOptions, - ) => Promise; - getDuckDuckGoSharedKey: (options?: StorageOptions) => Promise; - setDuckDuckGoSharedKey: (value: string, options?: StorageOptions) => Promise; + ): Promise; + abstract getDuckDuckGoSharedKey(options?: StorageOptions): Promise; + abstract setDuckDuckGoSharedKey(value: string, options?: StorageOptions): Promise; /** * @deprecated Use `TokenService.hasAccessToken$()` or `AuthService.authStatusFor$` instead. */ - getIsAuthenticated: (options?: StorageOptions) => Promise; + abstract getIsAuthenticated(options?: StorageOptions): Promise; /** * @deprecated Use `AccountService.activeAccount$` instead. */ - getUserId: (options?: StorageOptions) => Promise; + abstract getUserId(options?: StorageOptions): Promise; } diff --git a/libs/common/src/tools/password-strength/password-strength.service.abstraction.ts b/libs/common/src/tools/password-strength/password-strength.service.abstraction.ts index a49a6d481b5..ccc47d487a4 100644 --- a/libs/common/src/tools/password-strength/password-strength.service.abstraction.ts +++ b/libs/common/src/tools/password-strength/password-strength.service.abstraction.ts @@ -1,7 +1,9 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { ZXCVBNResult } from "zxcvbn"; export abstract class PasswordStrengthServiceAbstraction { - getPasswordStrength: (password: string, email?: string, userInputs?: string[]) => ZXCVBNResult; + abstract getPasswordStrength( + password: string, + email?: string, + userInputs?: string[], + ): ZXCVBNResult; } diff --git a/libs/common/src/tools/send/services/send-api.service.abstraction.ts b/libs/common/src/tools/send/services/send-api.service.abstraction.ts index 570f3e746a0..80c4410af11 100644 --- a/libs/common/src/tools/send/services/send-api.service.abstraction.ts +++ b/libs/common/src/tools/send/services/send-api.service.abstraction.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { ListResponse } from "../../../models/response/list.response"; import { EncArrayBuffer } from "../../../platform/models/domain/enc-array-buffer"; import { Send } from "../models/domain/send"; @@ -12,26 +10,29 @@ import { SendResponse } from "../models/response/send.response"; import { SendAccessView } from "../models/view/send-access.view"; export abstract class SendApiService { - getSend: (id: string) => Promise; - postSendAccess: ( + abstract getSend(id: string): Promise; + abstract postSendAccess( id: string, request: SendAccessRequest, apiUrl?: string, - ) => Promise; - getSends: () => Promise>; - postSend: (request: SendRequest) => Promise; - postFileTypeSend: (request: SendRequest) => Promise; - postSendFile: (sendId: string, fileId: string, data: FormData) => Promise; - putSend: (id: string, request: SendRequest) => Promise; - putSendRemovePassword: (id: string) => Promise; - deleteSend: (id: string) => Promise; - getSendFileDownloadData: ( + ): Promise; + abstract getSends(): Promise>; + abstract postSend(request: SendRequest): Promise; + abstract postFileTypeSend(request: SendRequest): Promise; + abstract postSendFile(sendId: string, fileId: string, data: FormData): Promise; + abstract putSend(id: string, request: SendRequest): Promise; + abstract putSendRemovePassword(id: string): Promise; + abstract deleteSend(id: string): Promise; + abstract getSendFileDownloadData( send: SendAccessView, request: SendAccessRequest, apiUrl?: string, - ) => Promise; - renewSendFileUploadUrl: (sendId: string, fileId: string) => Promise; - removePassword: (id: string) => Promise; - delete: (id: string) => Promise; - save: (sendData: [Send, EncArrayBuffer]) => Promise; + ): Promise; + abstract renewSendFileUploadUrl( + sendId: string, + fileId: string, + ): Promise; + abstract removePassword(id: string): Promise; + abstract delete(id: string): Promise; + abstract save(sendData: [Send, EncArrayBuffer]): Promise; } diff --git a/libs/common/src/tools/send/services/send.service.abstraction.ts b/libs/common/src/tools/send/services/send.service.abstraction.ts index f586e39a755..8301172477c 100644 --- a/libs/common/src/tools/send/services/send.service.abstraction.ts +++ b/libs/common/src/tools/send/services/send.service.abstraction.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Observable } from "rxjs"; // This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. @@ -16,49 +14,49 @@ import { SendWithIdRequest } from "../models/request/send-with-id.request"; import { SendView } from "../models/view/send.view"; export abstract class SendService implements UserKeyRotationDataProvider { - sends$: Observable; - sendViews$: Observable; + abstract sends$: Observable; + abstract sendViews$: Observable; - encrypt: ( + abstract encrypt( model: SendView, file: File | ArrayBuffer, password: string, key?: SymmetricCryptoKey, - ) => Promise<[Send, EncArrayBuffer]>; + ): Promise<[Send, EncArrayBuffer]>; /** * Provides a send for a determined id * updates after a change occurs to the send that matches the id * @param id The id of the desired send * @returns An observable that listens to the value of the desired send */ - get$: (id: string) => Observable; + abstract get$(id: string): Observable; /** * Provides re-encrypted user sends for the key rotation process * @param newUserKey The new user key to use for re-encryption * @throws Error if the new user key is null or undefined * @returns A list of user sends that have been re-encrypted with the new user key */ - getRotatedData: ( + abstract getRotatedData( originalUserKey: UserKey, newUserKey: UserKey, userId: UserId, - ) => Promise; + ): Promise; /** * @deprecated Do not call this, use the sends$ observable collection */ - getAll: () => Promise; + abstract getAll(): Promise; /** * @deprecated Only use in CLI */ - getFromState: (id: string) => Promise; + abstract getFromState(id: string): Promise; /** * @deprecated Only use in CLI */ - getAllDecryptedFromState: (userId: UserId) => Promise; + abstract getAllDecryptedFromState(userId: UserId): Promise; } export abstract class InternalSendService extends SendService { - upsert: (send: SendData | SendData[]) => Promise; - replace: (sends: { [id: string]: SendData }, userId: UserId) => Promise; - delete: (id: string | string[]) => Promise; + abstract upsert(send: SendData | SendData[]): Promise; + abstract replace(sends: { [id: string]: SendData }, userId: UserId): Promise; + abstract delete(id: string | string[]): Promise; } diff --git a/libs/common/src/vault/abstractions/cipher.service.ts b/libs/common/src/vault/abstractions/cipher.service.ts index 2f186369463..2f4fcf0ef51 100644 --- a/libs/common/src/vault/abstractions/cipher.service.ts +++ b/libs/common/src/vault/abstractions/cipher.service.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Observable } from "rxjs"; // This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. diff --git a/libs/common/src/vault/abstractions/file-upload/cipher-file-upload.service.ts b/libs/common/src/vault/abstractions/file-upload/cipher-file-upload.service.ts index 812439e2ca9..13c79241e36 100644 --- a/libs/common/src/vault/abstractions/file-upload/cipher-file-upload.service.ts +++ b/libs/common/src/vault/abstractions/file-upload/cipher-file-upload.service.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { EncString } from "../../../key-management/crypto/models/enc-string"; import { EncArrayBuffer } from "../../../platform/models/domain/enc-array-buffer"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; @@ -7,11 +5,11 @@ import { Cipher } from "../../models/domain/cipher"; import { CipherResponse } from "../../models/response/cipher.response"; export abstract class CipherFileUploadService { - upload: ( + abstract upload( cipher: Cipher, encFileName: EncString, encData: EncArrayBuffer, admin: boolean, dataEncKey: [SymmetricCryptoKey, EncString], - ) => Promise; + ): Promise; } diff --git a/libs/common/src/vault/abstractions/folder/folder-api.service.abstraction.ts b/libs/common/src/vault/abstractions/folder/folder-api.service.abstraction.ts index 1bb4a52e929..1b89f1664ca 100644 --- a/libs/common/src/vault/abstractions/folder/folder-api.service.abstraction.ts +++ b/libs/common/src/vault/abstractions/folder/folder-api.service.abstraction.ts @@ -1,14 +1,11 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore - import { UserId } from "../../../types/guid"; import { FolderData } from "../../models/data/folder.data"; import { Folder } from "../../models/domain/folder"; import { FolderResponse } from "../../models/response/folder.response"; -export class FolderApiServiceAbstraction { - save: (folder: Folder, userId: UserId) => Promise; - delete: (id: string, userId: UserId) => Promise; - get: (id: string) => Promise; - deleteAll: (userId: UserId) => Promise; +export abstract class FolderApiServiceAbstraction { + abstract save(folder: Folder, userId: UserId): Promise; + abstract delete(id: string, userId: UserId): Promise; + abstract get(id: string): Promise; + abstract deleteAll(userId: UserId): Promise; } diff --git a/libs/common/src/vault/abstractions/folder/folder.service.abstraction.ts b/libs/common/src/vault/abstractions/folder/folder.service.abstraction.ts index 7324fe22c8d..e56bfda32a4 100644 --- a/libs/common/src/vault/abstractions/folder/folder.service.abstraction.ts +++ b/libs/common/src/vault/abstractions/folder/folder.service.abstraction.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Observable } from "rxjs"; // This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. @@ -15,27 +13,27 @@ import { FolderWithIdRequest } from "../../models/request/folder-with-id.request import { FolderView } from "../../models/view/folder.view"; export abstract class FolderService implements UserKeyRotationDataProvider { - folders$: (userId: UserId) => Observable; - folderViews$: (userId: UserId) => Observable; + abstract folders$(userId: UserId): Observable; + abstract folderViews$(userId: UserId): Observable; - clearDecryptedFolderState: (userId: UserId) => Promise; - encrypt: (model: FolderView, key: SymmetricCryptoKey) => Promise; - get: (id: string, userId: UserId) => Promise; - getDecrypted$: (id: string, userId: UserId) => Observable; + abstract clearDecryptedFolderState(userId: UserId): Promise; + abstract encrypt(model: FolderView, key: SymmetricCryptoKey): Promise; + abstract get(id: string, userId: UserId): Promise; + abstract getDecrypted$(id: string, userId: UserId): Observable; /** * @deprecated Use firstValueFrom(folders$) directly instead * @param userId The user id * @returns Promise of folders array */ - getAllFromState: (userId: UserId) => Promise; + abstract getAllFromState(userId: UserId): Promise; /** * @deprecated Only use in CLI! */ - getFromState: (id: string, userId: UserId) => Promise; + abstract getFromState(id: string, userId: UserId): Promise; /** * @deprecated Only use in CLI! */ - getAllDecryptedFromState: (userId: UserId) => Promise; + abstract getAllDecryptedFromState(userId: UserId): Promise; /** * Returns user folders re-encrypted with the new user key. * @param originalUserKey the original user key @@ -44,16 +42,16 @@ export abstract class FolderService implements UserKeyRotationDataProvider Promise; + ): Promise; } export abstract class InternalFolderService extends FolderService { - upsert: (folder: FolderData | FolderData[], userId: UserId) => Promise; - replace: (folders: { [id: string]: FolderData }, userId: UserId) => Promise; - clear: (userId: UserId) => Promise; - delete: (id: string | string[], userId: UserId) => Promise; + abstract upsert(folder: FolderData | FolderData[], userId: UserId): Promise; + abstract replace(folders: { [id: string]: FolderData }, userId: UserId): Promise; + abstract clear(userId: UserId): Promise; + abstract delete(id: string | string[], userId: UserId): Promise; } diff --git a/libs/common/src/vault/abstractions/search.service.ts b/libs/common/src/vault/abstractions/search.service.ts index ed8bb2c3baf..57f301261c2 100644 --- a/libs/common/src/vault/abstractions/search.service.ts +++ b/libs/common/src/vault/abstractions/search.service.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Observable } from "rxjs"; import { SendView } from "../../tools/send/models/view/send.view"; @@ -8,25 +6,25 @@ import { CipherView } from "../models/view/cipher.view"; import { CipherViewLike } from "../utils/cipher-view-like-utils"; export abstract class SearchService { - indexedEntityId$: (userId: UserId) => Observable; + abstract indexedEntityId$(userId: UserId): Observable; - clearIndex: (userId: UserId) => Promise; - isSearchable: (userId: UserId, query: string) => Promise; - indexCiphers: ( + abstract clearIndex(userId: UserId): Promise; + abstract isSearchable(userId: UserId, query: string): Promise; + abstract indexCiphers( userId: UserId, ciphersToIndex: CipherView[], indexedEntityGuid?: string, - ) => Promise; - searchCiphers: ( + ): Promise; + abstract searchCiphers( userId: UserId, query: string, filter?: ((cipher: C) => boolean) | ((cipher: C) => boolean)[], ciphers?: C[], - ) => Promise; - searchCiphersBasic: ( + ): Promise; + abstract searchCiphersBasic( ciphers: C[], query: string, deleted?: boolean, - ) => C[]; - searchSends: (sends: SendView[], query: string) => SendView[]; + ): C[]; + abstract searchSends(sends: SendView[], query: string): SendView[]; } diff --git a/libs/common/src/vault/abstractions/vault-settings/vault-settings.service.ts b/libs/common/src/vault/abstractions/vault-settings/vault-settings.service.ts index ea1e73c2685..01b0011b7f7 100644 --- a/libs/common/src/vault/abstractions/vault-settings/vault-settings.service.ts +++ b/libs/common/src/vault/abstractions/vault-settings/vault-settings.service.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Observable } from "rxjs"; /** * Service for managing vault settings. @@ -9,42 +7,40 @@ export abstract class VaultSettingsService { * An observable monitoring the state of the enable passkeys setting. * The observable updates when the setting changes. */ - enablePasskeys$: Observable; + abstract enablePasskeys$: Observable; /** * An observable monitoring the state of the show cards on the current tab. */ - showCardsCurrentTab$: Observable; + abstract showCardsCurrentTab$: Observable; /** * An observable monitoring the state of the show identities on the current tab. */ - showIdentitiesCurrentTab$: Observable; - /** + abstract showIdentitiesCurrentTab$: Observable; /** * An observable monitoring the state of the click items on the Vault view * for Autofill suggestions. */ - clickItemsToAutofillVaultView$: Observable; - /** + abstract clickItemsToAutofillVaultView$: Observable; /** * Saves the enable passkeys setting to disk. * @param value The new value for the passkeys setting. */ - setEnablePasskeys: (value: boolean) => Promise; + abstract setEnablePasskeys(value: boolean): Promise; /** * Saves the show cards on tab page setting to disk. * @param value The new value for the show cards on tab page setting. */ - setShowCardsCurrentTab: (value: boolean) => Promise; + abstract setShowCardsCurrentTab(value: boolean): Promise; /** * Saves the show identities on tab page setting to disk. * @param value The new value for the show identities on tab page setting. */ - setShowIdentitiesCurrentTab: (value: boolean) => Promise; + abstract setShowIdentitiesCurrentTab(value: boolean): Promise; /** * Saves the click items on vault View for Autofill suggestions to disk. * @param value The new value for the click items on vault View for * Autofill suggestions setting. */ - setClickItemsToAutofillVaultView: (value: boolean) => Promise; + abstract setClickItemsToAutofillVaultView(value: boolean): Promise; } From 78353a988249bf109b4017b71ce8ebcc2393d496 Mon Sep 17 00:00:00 2001 From: Todd Martin <106564991+trmartin4@users.noreply.github.com> Date: Tue, 22 Jul 2025 14:04:23 -0400 Subject: [PATCH 08/21] fix(rpm): [PM-527] Remove build id links on rpm build --- apps/desktop/electron-builder.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/desktop/electron-builder.json b/apps/desktop/electron-builder.json index 832ab9d0bd3..800cdd848a7 100644 --- a/apps/desktop/electron-builder.json +++ b/apps/desktop/electron-builder.json @@ -240,7 +240,8 @@ "artifactName": "${productName}-${version}-${arch}.${ext}" }, "rpm": { - "artifactName": "${productName}-${version}-${arch}.${ext}" + "artifactName": "${productName}-${version}-${arch}.${ext}", + "fpm": ["--rpm-rpmbuild-define", "_build_id_links none"] }, "freebsd": { "artifactName": "${productName}-${version}-${arch}.${ext}" From d0fc9e9a2b1fbf5457736e5d80c5eb271187f638 Mon Sep 17 00:00:00 2001 From: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com> Date: Tue, 22 Jul 2025 13:19:26 -0500 Subject: [PATCH 09/21] [PM-19589] Update delete organization user event log message (#15714) * chore: update key and message with new content, refs PM-19589 * chore: update reference to new message key, refs PM-19589 * chore: update message based on product/design review, refs PM-19589 --- apps/web/src/app/core/event.service.ts | 4 ++-- apps/web/src/locales/en/messages.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/web/src/app/core/event.service.ts b/apps/web/src/app/core/event.service.ts index 14c87181f62..36d591cc390 100644 --- a/apps/web/src/app/core/event.service.ts +++ b/apps/web/src/app/core/event.service.ts @@ -342,9 +342,9 @@ export class EventService { ); break; case EventType.OrganizationUser_Deleted: - msg = this.i18nService.t("deletedUserId", this.formatOrgUserId(ev)); + msg = this.i18nService.t("deletedUserIdEventMessage", this.formatOrgUserId(ev)); humanReadableMsg = this.i18nService.t( - "deletedUserId", + "deletedUserIdEventMessage", this.getShortId(ev.organizationUserId), ); break; diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 9c9ecc79721..5d7cbd7d479 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -10295,8 +10295,8 @@ "organizationUserDeletedDesc": { "message": "The user was removed from the organization and all associated user data has been deleted." }, - "deletedUserId": { - "message": "Deleted user $ID$ - an owner / admin deleted the user account", + "deletedUserIdEventMessage": { + "message": "Deleted user $ID$", "placeholders": { "id": { "content": "$1", From 319528c647ad7e57cbc6f505ab390500696223c0 Mon Sep 17 00:00:00 2001 From: Miles Blackwood Date: Tue, 22 Jul 2025 14:45:59 -0400 Subject: [PATCH 10/21] Only call activeAccount$ when activeAccountStatus$ is Unlocked. (#15626) --- .../background/auto-submit-login.background.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/apps/browser/src/autofill/background/auto-submit-login.background.ts b/apps/browser/src/autofill/background/auto-submit-login.background.ts index dcafe21b63c..dfdfa0f4d67 100644 --- a/apps/browser/src/autofill/background/auto-submit-login.background.ts +++ b/apps/browser/src/autofill/background/auto-submit-login.background.ts @@ -1,12 +1,12 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { firstValueFrom, switchMap } from "rxjs"; +import { filter, firstValueFrom, of, switchMap } from "rxjs"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; import { getFirstPolicy } from "@bitwarden/common/admin-console/services/policy/default-policy.service"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; @@ -51,9 +51,14 @@ export class AutoSubmitLoginBackground implements AutoSubmitLoginBackgroundAbstr * Initializes the auto-submit login policy. If the policy is not enabled, it * will trigger a removal of any established listeners. */ + async init() { - this.accountService.activeAccount$ + this.authService.activeAccountStatus$ .pipe( + switchMap((value) => + value === AuthenticationStatus.Unlocked ? this.accountService.activeAccount$ : of(null), + ), + filter((account): account is Account => account !== null), getUserId, switchMap((userId) => this.policyService.policiesByType$(PolicyType.AutomaticAppLogIn, userId), From 53aaa2c285261e706bb11914b9ce5b85b589d6b0 Mon Sep 17 00:00:00 2001 From: Robyn MacCallum Date: Tue, 22 Jul 2025 15:41:33 -0400 Subject: [PATCH 11/21] Update tsconfig and package json (#15636) --- .../package-lock.json | 16 ++++++++++++++++ .../native-messaging-test-runner/package.json | 6 +++++- .../native-messaging-test-runner/tsconfig.json | 12 ++++++++++-- 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/apps/desktop/native-messaging-test-runner/package-lock.json b/apps/desktop/native-messaging-test-runner/package-lock.json index 37b8cf96ff3..043393df58b 100644 --- a/apps/desktop/native-messaging-test-runner/package-lock.json +++ b/apps/desktop/native-messaging-test-runner/package-lock.json @@ -10,7 +10,9 @@ "license": "GPL-3.0", "dependencies": { "@bitwarden/common": "file:../../../libs/common", + "@bitwarden/logging": "dist/libs/logging/src", "@bitwarden/node": "file:../../../libs/node", + "@bitwarden/storage-core": "file:../../../libs/storage-core", "module-alias": "2.2.3", "ts-node": "10.9.2", "uuid": "11.1.0", @@ -31,14 +33,28 @@ "version": "0.0.0", "license": "GPL-3.0" }, + "../../../libs/storage-core": { + "name": "@bitwarden/storage-core", + "version": "0.0.1", + "license": "GPL-3.0" + }, + "dist/libs/logging/src": {}, "node_modules/@bitwarden/common": { "resolved": "../../../libs/common", "link": true }, + "node_modules/@bitwarden/logging": { + "resolved": "dist/libs/logging/src", + "link": true + }, "node_modules/@bitwarden/node": { "resolved": "../../../libs/node", "link": true }, + "node_modules/@bitwarden/storage-core": { + "resolved": "../../../libs/storage-core", + "link": true + }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", diff --git a/apps/desktop/native-messaging-test-runner/package.json b/apps/desktop/native-messaging-test-runner/package.json index ea6b1b3e7a8..56e3e4edcf8 100644 --- a/apps/desktop/native-messaging-test-runner/package.json +++ b/apps/desktop/native-messaging-test-runner/package.json @@ -16,6 +16,8 @@ "dependencies": { "@bitwarden/common": "file:../../../libs/common", "@bitwarden/node": "file:../../../libs/node", + "@bitwarden/storage-core": "file:../../../libs/storage-core", + "@bitwarden/logging": "dist/libs/logging/src", "module-alias": "2.2.3", "ts-node": "10.9.2", "uuid": "11.1.0", @@ -27,6 +29,8 @@ }, "_moduleAliases": { "@bitwarden/common": "dist/libs/common/src", - "@bitwarden/node/services/node-crypto-function.service": "dist/libs/node/src/services/node-crypto-function.service" + "@bitwarden/node/services/node-crypto-function.service": "dist/libs/node/src/services/node-crypto-function.service", + "@bitwarden/storage-core": "dist/libs/storage-core/src", + "@bitwarden/logging": "dist/libs/logging/src" } } diff --git a/apps/desktop/native-messaging-test-runner/tsconfig.json b/apps/desktop/native-messaging-test-runner/tsconfig.json index 608e5a3bf4c..dcdf992f986 100644 --- a/apps/desktop/native-messaging-test-runner/tsconfig.json +++ b/apps/desktop/native-messaging-test-runner/tsconfig.json @@ -1,6 +1,6 @@ { - "extends": "../tsconfig", "compilerOptions": { + "baseUrl": "./", "outDir": "dist", "target": "es6", "module": "CommonJS", @@ -10,7 +10,15 @@ "sourceMap": false, "declaration": false, "paths": { - "@src/*": ["src/*"] + "@src/*": ["src/*"], + "@bitwarden/user-core": ["../../../libs/user-core/src/index.ts"], + "@bitwarden/storage-core": ["../../../libs/storage-core/src/index.ts"], + "@bitwarden/logging": ["../../../libs/logging/src/index.ts"], + "@bitwarden/admin-console/*": ["../../../libs/admin-console/src/*"], + "@bitwarden/auth/*": ["../../../libs/auth/src/*"], + "@bitwarden/common/*": ["../../../libs/common/src/*"], + "@bitwarden/key-management": ["../../../libs/key-management/src/"], + "@bitwarden/node/*": ["../../../libs/node/src/*"] }, "plugins": [ { From c2bbb7c0312f27bb6a5455d43f693e0ae1918495 Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Tue, 22 Jul 2025 21:59:42 +0200 Subject: [PATCH 12/21] Migrate vault abstract services to strict ts (#15731) --- .../src/vault/abstractions/deprecated-vault-filter.service.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/libs/angular/src/vault/abstractions/deprecated-vault-filter.service.ts b/libs/angular/src/vault/abstractions/deprecated-vault-filter.service.ts index 9a1a31b6068..30a4c6d4739 100644 --- a/libs/angular/src/vault/abstractions/deprecated-vault-filter.service.ts +++ b/libs/angular/src/vault/abstractions/deprecated-vault-filter.service.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Observable } from "rxjs"; // This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. From 54f0852f1a4c306fb5d087318ac429d7c3f10811 Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Tue, 22 Jul 2025 22:00:07 +0200 Subject: [PATCH 13/21] Migrate auth abstract services to strict ts (#15732) --- .../set-password-jit.service.abstraction.ts | 4 +-- .../auth-request.service.abstraction.ts | 36 +++++++++---------- .../abstractions/login-strategy.service.ts | 26 +++++++------- 3 files changed, 30 insertions(+), 36 deletions(-) diff --git a/libs/auth/src/angular/set-password-jit/set-password-jit.service.abstraction.ts b/libs/auth/src/angular/set-password-jit/set-password-jit.service.abstraction.ts index da6e9368007..92db88868a2 100644 --- a/libs/auth/src/angular/set-password-jit/set-password-jit.service.abstraction.ts +++ b/libs/auth/src/angular/set-password-jit/set-password-jit.service.abstraction.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { UserId } from "@bitwarden/common/types/guid"; import { MasterKey } from "@bitwarden/common/types/key"; import { KdfConfig } from "@bitwarden/key-management"; @@ -31,5 +29,5 @@ export abstract class SetPasswordJitService { * @throws If any property on the `credentials` object is null or undefined, or if a protectedUserKey * or newKeyPair could not be created. */ - setPassword: (credentials: SetPasswordCredentials) => Promise; + abstract setPassword(credentials: SetPasswordCredentials): Promise; } diff --git a/libs/auth/src/common/abstractions/auth-request.service.abstraction.ts b/libs/auth/src/common/abstractions/auth-request.service.abstraction.ts index 7e480c3a69c..9eea3fe7bb0 100644 --- a/libs/auth/src/common/abstractions/auth-request.service.abstraction.ts +++ b/libs/auth/src/common/abstractions/auth-request.service.abstraction.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Observable } from "rxjs"; import { AdminAuthRequestStorable } from "@bitwarden/common/auth/models/domain/admin-auth-req-storable"; @@ -10,20 +8,20 @@ import { UserKey, MasterKey } from "@bitwarden/common/types/key"; export abstract class AuthRequestServiceAbstraction { /** Emits an auth request id when an auth request has been approved. */ - authRequestPushNotification$: Observable; + abstract authRequestPushNotification$: Observable; /** * Emits when a login has been approved by an admin. This emission is specifically for the * purpose of notifying the consuming component to display a toast informing the user. */ - adminLoginApproved$: Observable; + abstract adminLoginApproved$: Observable; /** * Returns an admin auth request for the given user if it exists. * @param userId The user id. * @throws If `userId` is not provided. */ - abstract getAdminAuthRequest: (userId: UserId) => Promise; + abstract getAdminAuthRequest(userId: UserId): Promise; /** * Sets an admin auth request for the given user. * Note: use {@link clearAdminAuthRequest} to clear the request. @@ -31,16 +29,16 @@ export abstract class AuthRequestServiceAbstraction { * @param userId The user id. * @throws If `authRequest` or `userId` is not provided. */ - abstract setAdminAuthRequest: ( + abstract setAdminAuthRequest( authRequest: AdminAuthRequestStorable, userId: UserId, - ) => Promise; + ): Promise; /** * Clears an admin auth request for the given user. * @param userId The user id. * @throws If `userId` is not provided. */ - abstract clearAdminAuthRequest: (userId: UserId) => Promise; + abstract clearAdminAuthRequest(userId: UserId): Promise; /** * Gets a list of standard pending auth requests for the user. * @returns An observable of an array of auth request. @@ -61,42 +59,42 @@ export abstract class AuthRequestServiceAbstraction { * approval was successful. * @throws If the auth request is missing an id or key. */ - abstract approveOrDenyAuthRequest: ( + abstract approveOrDenyAuthRequest( approve: boolean, authRequest: AuthRequestResponse, - ) => Promise; + ): Promise; /** * Sets the `UserKey` from an auth request. Auth request must have a `UserKey`. * @param authReqResponse The auth request. * @param authReqPrivateKey The private key corresponding to the public key sent in the auth request. * @param userId The ID of the user for whose account we will set the key. */ - abstract setUserKeyAfterDecryptingSharedUserKey: ( + abstract setUserKeyAfterDecryptingSharedUserKey( authReqResponse: AuthRequestResponse, authReqPrivateKey: ArrayBuffer, userId: UserId, - ) => Promise; + ): Promise; /** * Sets the `MasterKey` and `MasterKeyHash` from an auth request. Auth request must have a `MasterKey` and `MasterKeyHash`. * @param authReqResponse The auth request. * @param authReqPrivateKey The private key corresponding to the public key sent in the auth request. * @param userId The ID of the user for whose account we will set the keys. */ - abstract setKeysAfterDecryptingSharedMasterKeyAndHash: ( + abstract setKeysAfterDecryptingSharedMasterKeyAndHash( authReqResponse: AuthRequestResponse, authReqPrivateKey: ArrayBuffer, userId: UserId, - ) => Promise; + ): Promise; /** * Decrypts a `UserKey` from a public key encrypted `UserKey`. * @param pubKeyEncryptedUserKey The public key encrypted `UserKey`. * @param privateKey The private key corresponding to the public key used to encrypt the `UserKey`. * @returns The decrypted `UserKey`. */ - abstract decryptPubKeyEncryptedUserKey: ( + abstract decryptPubKeyEncryptedUserKey( pubKeyEncryptedUserKey: string, privateKey: ArrayBuffer, - ) => Promise; + ): Promise; /** * Decrypts a `MasterKey` and `MasterKeyHash` from a public key encrypted `MasterKey` and `MasterKeyHash`. * @param pubKeyEncryptedMasterKey The public key encrypted `MasterKey`. @@ -104,18 +102,18 @@ export abstract class AuthRequestServiceAbstraction { * @param privateKey The private key corresponding to the public key used to encrypt the `MasterKey` and `MasterKeyHash`. * @returns The decrypted `MasterKey` and `MasterKeyHash`. */ - abstract decryptPubKeyEncryptedMasterKeyAndHash: ( + abstract decryptPubKeyEncryptedMasterKeyAndHash( pubKeyEncryptedMasterKey: string, pubKeyEncryptedMasterKeyHash: string, privateKey: ArrayBuffer, - ) => Promise<{ masterKey: MasterKey; masterKeyHash: string }>; + ): Promise<{ masterKey: MasterKey; masterKeyHash: string }>; /** * Handles incoming auth request push notifications. * @param notification push notification. * @remark We should only be receiving approved push notifications to prevent enumeration. */ - abstract sendAuthRequestPushNotification: (notification: AuthRequestPushNotification) => void; + abstract sendAuthRequestPushNotification(notification: AuthRequestPushNotification): void; /** * Creates a dash-delimited fingerprint for use in confirming the `AuthRequest` between the requesting and approving device. diff --git a/libs/auth/src/common/abstractions/login-strategy.service.ts b/libs/auth/src/common/abstractions/login-strategy.service.ts index b0fffae2ab4..64854393240 100644 --- a/libs/auth/src/common/abstractions/login-strategy.service.ts +++ b/libs/auth/src/common/abstractions/login-strategy.service.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Observable } from "rxjs"; import { AuthenticationType } from "@bitwarden/common/auth/enums/authentication-type"; @@ -20,60 +18,60 @@ export abstract class LoginStrategyServiceAbstraction { * The current strategy being used to authenticate. * Emits null if the session has timed out. */ - currentAuthType$: Observable; + abstract currentAuthType$: Observable; /** * If the login strategy uses the email address of the user, this * will return it. Otherwise, it will return null. */ - getEmail: () => Promise; + abstract getEmail(): Promise; /** * If the user is logging in with a master password, this will return * the master password hash. Otherwise, it will return null. */ - getMasterPasswordHash: () => Promise; + abstract getMasterPasswordHash(): Promise; /** * If the user is logging in with SSO, this will return * the email auth token. Otherwise, it will return null. * @see {@link SsoLoginStrategyData.ssoEmail2FaSessionToken} */ - getSsoEmail2FaSessionToken: () => Promise; + abstract getSsoEmail2FaSessionToken(): Promise; /** * Returns the access code if the user is logging in with an * Auth Request. Otherwise, it will return null. */ - getAccessCode: () => Promise; + abstract getAccessCode(): Promise; /** * Returns the auth request ID if the user is logging in with an * Auth Request. Otherwise, it will return null. */ - getAuthRequestId: () => Promise; + abstract getAuthRequestId(): Promise; /** * Sends a token request to the server using the provided credentials. */ - logIn: ( + abstract logIn( credentials: | UserApiLoginCredentials | PasswordLoginCredentials | SsoLoginCredentials | AuthRequestLoginCredentials | WebAuthnLoginCredentials, - ) => Promise; + ): Promise; /** * Sends a token request to the server with the provided two factor token. * This uses data stored from {@link LoginStrategyServiceAbstraction.logIn}, so that must be called first. * Returns an error if no session data is found. */ - logInTwoFactor: (twoFactor: TokenTwoFactorRequest) => Promise; + abstract logInTwoFactor(twoFactor: TokenTwoFactorRequest): Promise; /** * Creates a master key from the provided master password and email. */ - makePreloginKey: (masterPassword: string, email: string) => Promise; + abstract makePreloginKey(masterPassword: string, email: string): Promise; /** * Emits true if the authentication session has expired. */ - authenticationSessionTimeout$: Observable; + abstract get authenticationSessionTimeout$(): Observable; /** * Sends a token request to the server with the provided device verification OTP. */ - logInNewDeviceVerification: (deviceVerificationOtp: string) => Promise; + abstract logInNewDeviceVerification(deviceVerificationOtp: string): Promise; } From c37965174b4288789f06ff9be425da70c6d92e86 Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Tue, 22 Jul 2025 22:00:24 +0200 Subject: [PATCH 14/21] Migrate platform owned abstract service to strict ts (#15734) --- .../fido2-active-request-manager.abstraction.ts | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/libs/common/src/platform/abstractions/fido2/fido2-active-request-manager.abstraction.ts b/libs/common/src/platform/abstractions/fido2/fido2-active-request-manager.abstraction.ts index ffb78d51bd3..390a6f4e5bd 100644 --- a/libs/common/src/platform/abstractions/fido2/fido2-active-request-manager.abstraction.ts +++ b/libs/common/src/platform/abstractions/fido2/fido2-active-request-manager.abstraction.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Observable, Subject } from "rxjs"; import { Fido2CredentialView } from "../../../vault/models/view/fido2-credential.view"; @@ -25,13 +23,13 @@ export interface ActiveRequest { export type RequestCollection = Readonly<{ [tabId: number]: ActiveRequest }>; export abstract class Fido2ActiveRequestManager { - getActiveRequest$: (tabId: number) => Observable; - getActiveRequest: (tabId: number) => ActiveRequest | undefined; - newActiveRequest: ( + abstract getActiveRequest$(tabId: number): Observable; + abstract getActiveRequest(tabId: number): ActiveRequest | undefined; + abstract newActiveRequest( tabId: number, credentials: Fido2CredentialView[], abortController: AbortController, - ) => Promise; - removeActiveRequest: (tabId: number) => void; - removeAllActiveRequests: () => void; + ): Promise; + abstract removeActiveRequest(tabId: number): void; + abstract removeAllActiveRequests(): void; } From 643d0c9a4c8eeafb36bd103e9ff84cfae2cb6c23 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 22 Jul 2025 15:56:08 -0700 Subject: [PATCH 15/21] [deps] Vault: Update form-data to v4.0.4 [SECURITY] (#15712) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- apps/cli/package.json | 2 +- package-lock.json | 12 +++++++----- package.json | 2 +- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/apps/cli/package.json b/apps/cli/package.json index 54855d72104..0d3c151f012 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -70,7 +70,7 @@ "chalk": "4.1.2", "commander": "11.1.0", "core-js": "3.44.0", - "form-data": "4.0.2", + "form-data": "4.0.4", "https-proxy-agent": "7.0.6", "inquirer": "8.2.6", "jsdom": "26.1.0", diff --git a/package-lock.json b/package-lock.json index fbbc4c25b44..85bd2a0efb0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -43,7 +43,7 @@ "chalk": "4.1.2", "commander": "11.1.0", "core-js": "3.44.0", - "form-data": "4.0.2", + "form-data": "4.0.4", "https-proxy-agent": "7.0.6", "inquirer": "8.2.6", "jsdom": "26.1.0", @@ -206,7 +206,7 @@ "chalk": "4.1.2", "commander": "11.1.0", "core-js": "3.44.0", - "form-data": "4.0.2", + "form-data": "4.0.4", "https-proxy-agent": "7.0.6", "inquirer": "8.2.6", "jsdom": "26.1.0", @@ -358,6 +358,7 @@ "license": "GPL-3.0" }, "libs/messaging-internal": { + "name": "@bitwarden/messaging-internal", "version": "0.0.1", "license": "GPL-3.0" }, @@ -21194,14 +21195,15 @@ } }, "node_modules/form-data": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", - "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", "mime-types": "^2.1.12" }, "engines": { diff --git a/package.json b/package.json index 2cb60a6afd1..bcf4f326531 100644 --- a/package.json +++ b/package.json @@ -178,7 +178,7 @@ "chalk": "4.1.2", "commander": "11.1.0", "core-js": "3.44.0", - "form-data": "4.0.2", + "form-data": "4.0.4", "https-proxy-agent": "7.0.6", "inquirer": "8.2.6", "jsdom": "26.1.0", From 2f47add6f157db355603c072fbde9cd4881b4ad0 Mon Sep 17 00:00:00 2001 From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> Date: Tue, 22 Jul 2025 19:08:09 -0500 Subject: [PATCH 16/21] [PM-23596] Redirect to `/setup-extension` (#15641) * remove current redirection from auth code * update timeouts of the web browser interaction * add guard for setup-extension page * decrease timeout to 25ms * avoid redirection for mobile users + add tests * add tests * condense variables * catch error from profile fetch --------- Co-authored-by: Shane Melton --- .../web-registration-finish.service.spec.ts | 22 --- .../web-registration-finish.service.ts | 15 -- apps/web/src/app/core/core.module.ts | 1 - apps/web/src/app/oss-routing.module.ts | 2 + .../add-extension-later-dialog.component.html | 8 +- ...d-extension-later-dialog.component.spec.ts | 14 ++ .../add-extension-later-dialog.component.ts | 17 ++- .../setup-extension.component.spec.ts | 16 +++ .../setup-extension.component.ts | 30 +++- .../setup-extension-redirect.guard.spec.ts | 132 ++++++++++++++++++ .../guards/setup-extension-redirect.guard.ts | 109 +++++++++++++++ .../web-browser-interaction.service.spec.ts | 6 +- .../web-browser-interaction.service.ts | 17 ++- .../default-registration-finish.service.ts | 4 - .../registration-finish.component.ts | 3 +- .../registration-finish.service.ts | 5 - .../src/platform/state/state-definitions.ts | 7 + 17 files changed, 347 insertions(+), 61 deletions(-) create mode 100644 apps/web/src/app/vault/guards/setup-extension-redirect.guard.spec.ts create mode 100644 apps/web/src/app/vault/guards/setup-extension-redirect.guard.ts diff --git a/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.spec.ts b/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.spec.ts index 69a2f27a322..845df89622b 100644 --- a/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.spec.ts +++ b/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.spec.ts @@ -12,7 +12,6 @@ import { AccountApiService } from "@bitwarden/common/auth/abstractions/account-a import { OrganizationInvite } from "@bitwarden/common/auth/services/organization-invite/organization-invite"; import { OrganizationInviteService } from "@bitwarden/common/auth/services/organization-invite/organization-invite.service"; import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { CsprngArray } from "@bitwarden/common/types/csprng"; @@ -30,7 +29,6 @@ describe("WebRegistrationFinishService", () => { let policyApiService: MockProxy; let logService: MockProxy; let policyService: MockProxy; - let configService: MockProxy; beforeEach(() => { keyService = mock(); @@ -39,7 +37,6 @@ describe("WebRegistrationFinishService", () => { policyApiService = mock(); logService = mock(); policyService = mock(); - configService = mock(); service = new WebRegistrationFinishService( keyService, @@ -48,7 +45,6 @@ describe("WebRegistrationFinishService", () => { policyApiService, logService, policyService, - configService, ); }); @@ -414,22 +410,4 @@ describe("WebRegistrationFinishService", () => { ); }); }); - - describe("determineLoginSuccessRoute", () => { - it("returns /setup-extension when the end user activation feature flag is enabled", async () => { - configService.getFeatureFlag.mockResolvedValue(true); - - const result = await service.determineLoginSuccessRoute(); - - expect(result).toBe("/setup-extension"); - }); - - it("returns /vault when the end user activation feature flag is disabled", async () => { - configService.getFeatureFlag.mockResolvedValue(false); - - const result = await service.determineLoginSuccessRoute(); - - expect(result).toBe("/vault"); - }); - }); }); diff --git a/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.ts b/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.ts index a3774e87db8..a9eba08be8c 100644 --- a/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.ts +++ b/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.ts @@ -14,12 +14,10 @@ import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; import { AccountApiService } from "@bitwarden/common/auth/abstractions/account-api.service"; import { RegisterFinishRequest } from "@bitwarden/common/auth/models/request/registration/register-finish.request"; import { OrganizationInviteService } from "@bitwarden/common/auth/services/organization-invite/organization-invite.service"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { EncryptedString, EncString, } from "@bitwarden/common/key-management/crypto/models/enc-string"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { KeyService } from "@bitwarden/key-management"; @@ -34,7 +32,6 @@ export class WebRegistrationFinishService private policyApiService: PolicyApiServiceAbstraction, private logService: LogService, private policyService: PolicyService, - private configService: ConfigService, ) { super(keyService, accountApiService); } @@ -79,18 +76,6 @@ export class WebRegistrationFinishService return masterPasswordPolicyOpts; } - override async determineLoginSuccessRoute(): Promise { - const endUserActivationFlagEnabled = await this.configService.getFeatureFlag( - FeatureFlag.PM19315EndUserActivationMvp, - ); - - if (endUserActivationFlagEnabled) { - return "/setup-extension"; - } else { - return super.determineLoginSuccessRoute(); - } - } - // Note: the org invite token and email verification are mutually exclusive. Only one will be present. override async buildRegisterRequest( email: string, diff --git a/apps/web/src/app/core/core.module.ts b/apps/web/src/app/core/core.module.ts index d98a2ee8cf2..7fe8ef4c79f 100644 --- a/apps/web/src/app/core/core.module.ts +++ b/apps/web/src/app/core/core.module.ts @@ -264,7 +264,6 @@ const safeProviders: SafeProvider[] = [ PolicyApiServiceAbstraction, LogService, PolicyService, - ConfigService, ], }), safeProvider({ diff --git a/apps/web/src/app/oss-routing.module.ts b/apps/web/src/app/oss-routing.module.ts index 8a2270113a9..1fb19757d60 100644 --- a/apps/web/src/app/oss-routing.module.ts +++ b/apps/web/src/app/oss-routing.module.ts @@ -83,6 +83,7 @@ import { SendComponent } from "./tools/send/send.component"; import { BrowserExtensionPromptInstallComponent } from "./vault/components/browser-extension-prompt/browser-extension-prompt-install.component"; import { BrowserExtensionPromptComponent } from "./vault/components/browser-extension-prompt/browser-extension-prompt.component"; import { SetupExtensionComponent } from "./vault/components/setup-extension/setup-extension.component"; +import { setupExtensionRedirectGuard } from "./vault/guards/setup-extension-redirect.guard"; import { VaultModule } from "./vault/individual-vault/vault.module"; const routes: Routes = [ @@ -628,6 +629,7 @@ const routes: Routes = [ children: [ { path: "vault", + canActivate: [setupExtensionRedirectGuard], loadChildren: () => VaultModule, }, { diff --git a/apps/web/src/app/vault/components/setup-extension/add-extension-later-dialog.component.html b/apps/web/src/app/vault/components/setup-extension/add-extension-later-dialog.component.html index df1786e227e..560bd5fd464 100644 --- a/apps/web/src/app/vault/components/setup-extension/add-extension-later-dialog.component.html +++ b/apps/web/src/app/vault/components/setup-extension/add-extension-later-dialog.component.html @@ -18,7 +18,13 @@ > {{ "getTheExtension" | i18n }} - + {{ "skipToWebApp" | i18n }} diff --git a/apps/web/src/app/vault/components/setup-extension/add-extension-later-dialog.component.spec.ts b/apps/web/src/app/vault/components/setup-extension/add-extension-later-dialog.component.spec.ts index d34dba737dd..a5d5ec4b939 100644 --- a/apps/web/src/app/vault/components/setup-extension/add-extension-later-dialog.component.spec.ts +++ b/apps/web/src/app/vault/components/setup-extension/add-extension-later-dialog.component.spec.ts @@ -1,3 +1,4 @@ +import { DialogRef } from "@angular/cdk/dialog"; import { ComponentFixture, TestBed } from "@angular/core/testing"; import { By } from "@angular/platform-browser"; import { provideNoopAnimations } from "@angular/platform-browser/animations"; @@ -5,20 +6,26 @@ import { RouterModule } from "@angular/router"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { DIALOG_DATA } from "@bitwarden/components"; import { AddExtensionLaterDialogComponent } from "./add-extension-later-dialog.component"; describe("AddExtensionLaterDialogComponent", () => { let fixture: ComponentFixture; const getDevice = jest.fn().mockReturnValue(null); + const onDismiss = jest.fn(); beforeEach(async () => { + onDismiss.mockClear(); + await TestBed.configureTestingModule({ imports: [AddExtensionLaterDialogComponent, RouterModule.forRoot([])], providers: [ provideNoopAnimations(), { provide: PlatformUtilsService, useValue: { getDevice } }, { provide: I18nService, useValue: { t: (key: string) => key } }, + { provide: DialogRef, useValue: { close: jest.fn() } }, + { provide: DIALOG_DATA, useValue: { onDismiss } }, ], }).compileComponents(); @@ -39,4 +46,11 @@ describe("AddExtensionLaterDialogComponent", () => { expect(skipLink.attributes.href).toBe("/vault"); }); + + it('invokes `onDismiss` when "Skip to Web App" is clicked', () => { + const skipLink = fixture.debugElement.queryAll(By.css("a[bitButton]"))[1]; + skipLink.triggerEventHandler("click", {}); + + expect(onDismiss).toHaveBeenCalled(); + }); }); diff --git a/apps/web/src/app/vault/components/setup-extension/add-extension-later-dialog.component.ts b/apps/web/src/app/vault/components/setup-extension/add-extension-later-dialog.component.ts index 3324cb8b1b0..5f4e3f586f5 100644 --- a/apps/web/src/app/vault/components/setup-extension/add-extension-later-dialog.component.ts +++ b/apps/web/src/app/vault/components/setup-extension/add-extension-later-dialog.component.ts @@ -4,7 +4,17 @@ import { RouterModule } from "@angular/router"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { getWebStoreUrl } from "@bitwarden/common/vault/utils/get-web-store-url"; -import { ButtonComponent, DialogModule, TypographyModule } from "@bitwarden/components"; +import { + ButtonComponent, + DIALOG_DATA, + DialogModule, + TypographyModule, +} from "@bitwarden/components"; + +export type AddExtensionLaterDialogData = { + /** Method invoked when the dialog is dismissed */ + onDismiss: () => void; +}; @Component({ selector: "vault-add-extension-later-dialog", @@ -13,6 +23,7 @@ import { ButtonComponent, DialogModule, TypographyModule } from "@bitwarden/comp }) export class AddExtensionLaterDialogComponent implements OnInit { private platformUtilsService = inject(PlatformUtilsService); + private data: AddExtensionLaterDialogData = inject(DIALOG_DATA); /** Download Url for the extension based on the browser */ protected webStoreUrl: string = ""; @@ -20,4 +31,8 @@ export class AddExtensionLaterDialogComponent implements OnInit { ngOnInit(): void { this.webStoreUrl = getWebStoreUrl(this.platformUtilsService.getDevice()); } + + async dismissExtensionPage() { + this.data.onDismiss(); + } } diff --git a/apps/web/src/app/vault/components/setup-extension/setup-extension.component.spec.ts b/apps/web/src/app/vault/components/setup-extension/setup-extension.component.spec.ts index 752e2c8d4a6..e824cd92f37 100644 --- a/apps/web/src/app/vault/components/setup-extension/setup-extension.component.spec.ts +++ b/apps/web/src/app/vault/components/setup-extension/setup-extension.component.spec.ts @@ -3,12 +3,14 @@ import { By } from "@angular/platform-browser"; import { Router, RouterModule } from "@angular/router"; import { BehaviorSubject } from "rxjs"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { DeviceType } from "@bitwarden/common/enums"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { StateProvider } from "@bitwarden/common/platform/state"; import { WebBrowserInteractionService } from "../../services/web-browser-interaction.service"; @@ -21,11 +23,13 @@ describe("SetupExtensionComponent", () => { const getFeatureFlag = jest.fn().mockResolvedValue(false); const navigate = jest.fn().mockResolvedValue(true); const openExtension = jest.fn().mockResolvedValue(true); + const update = jest.fn().mockResolvedValue(true); const extensionInstalled$ = new BehaviorSubject(null); beforeEach(async () => { navigate.mockClear(); openExtension.mockClear(); + update.mockClear(); getFeatureFlag.mockClear().mockResolvedValue(true); window.matchMedia = jest.fn().mockReturnValue(false); @@ -36,6 +40,14 @@ describe("SetupExtensionComponent", () => { { provide: ConfigService, useValue: { getFeatureFlag } }, { provide: WebBrowserInteractionService, useValue: { extensionInstalled$, openExtension } }, { provide: PlatformUtilsService, useValue: { getDevice: () => DeviceType.UnknownBrowser } }, + { + provide: AccountService, + useValue: { activeAccount$: new BehaviorSubject({ account: { id: "account-id" } }) }, + }, + { + provide: StateProvider, + useValue: { getUser: () => ({ update }) }, + }, ], }).compileComponents(); @@ -120,6 +132,10 @@ describe("SetupExtensionComponent", () => { expect(openExtension).toHaveBeenCalled(); }); + + it("dismisses the extension page", () => { + expect(update).toHaveBeenCalledTimes(1); + }); }); }); }); diff --git a/apps/web/src/app/vault/components/setup-extension/setup-extension.component.ts b/apps/web/src/app/vault/components/setup-extension/setup-extension.component.ts index 9ee8e189627..14770ca5d6c 100644 --- a/apps/web/src/app/vault/components/setup-extension/setup-extension.component.ts +++ b/apps/web/src/app/vault/components/setup-extension/setup-extension.component.ts @@ -2,13 +2,16 @@ import { DOCUMENT, NgIf } from "@angular/common"; import { Component, DestroyRef, inject, OnDestroy, OnInit } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { Router, RouterModule } from "@angular/router"; -import { pairwise, startWith } from "rxjs"; +import { firstValueFrom, pairwise, startWith } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { StateProvider } from "@bitwarden/common/platform/state"; import { UnionOfValues } from "@bitwarden/common/vault/types/union-of-values"; import { getWebStoreUrl } from "@bitwarden/common/vault/utils/get-web-store-url"; import { @@ -20,9 +23,13 @@ import { } from "@bitwarden/components"; import { VaultIcons } from "@bitwarden/vault"; +import { SETUP_EXTENSION_DISMISSED } from "../../guards/setup-extension-redirect.guard"; import { WebBrowserInteractionService } from "../../services/web-browser-interaction.service"; -import { AddExtensionLaterDialogComponent } from "./add-extension-later-dialog.component"; +import { + AddExtensionLaterDialogComponent, + AddExtensionLaterDialogData, +} from "./add-extension-later-dialog.component"; import { AddExtensionVideosComponent } from "./add-extension-videos.component"; const SetupExtensionState = { @@ -53,6 +60,8 @@ export class SetupExtensionComponent implements OnInit, OnDestroy { private destroyRef = inject(DestroyRef); private platformUtilsService = inject(PlatformUtilsService); private dialogService = inject(DialogService); + private stateProvider = inject(StateProvider); + private accountService = inject(AccountService); private document = inject(DOCUMENT); protected SetupExtensionState = SetupExtensionState; @@ -96,6 +105,7 @@ export class SetupExtensionComponent implements OnInit, OnDestroy { // Extension was not installed and now it is, show success state if (previousState === false && currentState) { this.dialogRef?.close(); + void this.dismissExtensionPage(); this.state = SetupExtensionState.Success; } @@ -125,17 +135,31 @@ export class SetupExtensionComponent implements OnInit, OnDestroy { const isMobile = Utils.isMobileBrowser; if (!isFeatureEnabled || isMobile) { + await this.dismissExtensionPage(); await this.router.navigate(["/vault"]); } } /** Opens the add extension later dialog */ addItLater() { - this.dialogRef = this.dialogService.open(AddExtensionLaterDialogComponent); + this.dialogRef = this.dialogService.open( + AddExtensionLaterDialogComponent, + { + data: { + onDismiss: this.dismissExtensionPage.bind(this), + }, + }, + ); } /** Opens the browser extension */ openExtension() { void this.webBrowserExtensionInteractionService.openExtension(); } + + /** Update local state to never show this page again. */ + private async dismissExtensionPage() { + const accountId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + void this.stateProvider.getUser(accountId, SETUP_EXTENSION_DISMISSED).update(() => true); + } } diff --git a/apps/web/src/app/vault/guards/setup-extension-redirect.guard.spec.ts b/apps/web/src/app/vault/guards/setup-extension-redirect.guard.spec.ts new file mode 100644 index 00000000000..e6fc03fd844 --- /dev/null +++ b/apps/web/src/app/vault/guards/setup-extension-redirect.guard.spec.ts @@ -0,0 +1,132 @@ +import { TestBed } from "@angular/core/testing"; +import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from "@angular/router"; +import { BehaviorSubject } from "rxjs"; + +import { VaultProfileService } from "@bitwarden/angular/vault/services/vault-profile.service"; +import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { StateProvider } from "@bitwarden/common/platform/state"; + +import { WebBrowserInteractionService } from "../services/web-browser-interaction.service"; + +import { setupExtensionRedirectGuard } from "./setup-extension-redirect.guard"; + +describe("setupExtensionRedirectGuard", () => { + const _state = Object.freeze({}) as RouterStateSnapshot; + const emptyRoute = Object.freeze({ queryParams: {} }) as ActivatedRouteSnapshot; + const seventeenDaysAgo = new Date(); + seventeenDaysAgo.setDate(seventeenDaysAgo.getDate() - 17); + + const account = { + id: "account-id", + } as unknown as Account; + + const activeAccount$ = new BehaviorSubject(account); + const extensionInstalled$ = new BehaviorSubject(false); + const state$ = new BehaviorSubject(false); + const createUrlTree = jest.fn(); + const getFeatureFlag = jest.fn().mockImplementation((key) => { + if (key === FeatureFlag.PM19315EndUserActivationMvp) { + return Promise.resolve(true); + } + + return Promise.resolve(false); + }); + const getProfileCreationDate = jest.fn().mockResolvedValue(seventeenDaysAgo); + + beforeEach(() => { + Utils.isMobileBrowser = false; + + getFeatureFlag.mockClear(); + getProfileCreationDate.mockClear(); + createUrlTree.mockClear(); + + TestBed.configureTestingModule({ + providers: [ + { provide: Router, useValue: { createUrlTree } }, + { provide: ConfigService, useValue: { getFeatureFlag } }, + { provide: AccountService, useValue: { activeAccount$ } }, + { provide: StateProvider, useValue: { getUser: () => ({ state$ }) } }, + { provide: WebBrowserInteractionService, useValue: { extensionInstalled$ } }, + { + provide: VaultProfileService, + useValue: { getProfileCreationDate }, + }, + ], + }); + }); + + function setupExtensionGuard(route?: ActivatedRouteSnapshot) { + // Run the guard within injection context so `inject` works as you'd expect + // Pass state object to make TypeScript happy + return TestBed.runInInjectionContext(async () => + setupExtensionRedirectGuard(route ?? emptyRoute, _state), + ); + } + + it("returns `true` when the profile was created more than 30 days ago", async () => { + const thirtyOneDaysAgo = new Date(); + thirtyOneDaysAgo.setDate(thirtyOneDaysAgo.getDate() - 31); + + getProfileCreationDate.mockResolvedValueOnce(thirtyOneDaysAgo); + + expect(await setupExtensionGuard()).toBe(true); + }); + + it("returns `true` when the profile check fails", async () => { + getProfileCreationDate.mockRejectedValueOnce(new Error("Profile check failed")); + + expect(await setupExtensionGuard()).toBe(true); + }); + + it("returns `true` when the feature flag is disabled", async () => { + getFeatureFlag.mockResolvedValueOnce(false); + + expect(await setupExtensionGuard()).toBe(true); + }); + + it("returns `true` when the user is on a mobile device", async () => { + Utils.isMobileBrowser = true; + + expect(await setupExtensionGuard()).toBe(true); + }); + + it("returns `true` when the user has dismissed the extension page", async () => { + state$.next(true); + + expect(await setupExtensionGuard()).toBe(true); + }); + + it("returns `true` when the user has the extension installed", async () => { + state$.next(false); + extensionInstalled$.next(true); + + expect(await setupExtensionGuard()).toBe(true); + }); + + it('redirects the user to "/setup-extension" when all criteria do not pass', async () => { + state$.next(false); + extensionInstalled$.next(false); + + await setupExtensionGuard(); + + expect(createUrlTree).toHaveBeenCalledWith(["/setup-extension"]); + }); + + describe("missing current account", () => { + afterAll(() => { + // reset `activeAccount$` observable + activeAccount$.next(account); + }); + + it("redirects to login when account is missing", async () => { + activeAccount$.next(null); + + await setupExtensionGuard(); + + expect(createUrlTree).toHaveBeenCalledWith(["/login"]); + }); + }); +}); diff --git a/apps/web/src/app/vault/guards/setup-extension-redirect.guard.ts b/apps/web/src/app/vault/guards/setup-extension-redirect.guard.ts new file mode 100644 index 00000000000..983fd8ed0aa --- /dev/null +++ b/apps/web/src/app/vault/guards/setup-extension-redirect.guard.ts @@ -0,0 +1,109 @@ +import { inject } from "@angular/core"; +import { CanActivateFn, Router } from "@angular/router"; +import { firstValueFrom, map } from "rxjs"; + +import { VaultProfileService } from "@bitwarden/angular/vault/services/vault-profile.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { + SETUP_EXTENSION_DISMISSED_DISK, + StateProvider, + UserKeyDefinition, +} from "@bitwarden/common/platform/state"; + +import { WebBrowserInteractionService } from "../services/web-browser-interaction.service"; + +export const SETUP_EXTENSION_DISMISSED = new UserKeyDefinition( + SETUP_EXTENSION_DISMISSED_DISK, + "setupExtensionDismissed", + { + deserializer: (dismissed) => dismissed, + clearOn: [], + }, +); + +export const setupExtensionRedirectGuard: CanActivateFn = async () => { + const router = inject(Router); + const configService = inject(ConfigService); + const accountService = inject(AccountService); + const vaultProfileService = inject(VaultProfileService); + const stateProvider = inject(StateProvider); + const webBrowserInteractionService = inject(WebBrowserInteractionService); + + const isMobile = Utils.isMobileBrowser; + + const endUserFeatureEnabled = await configService.getFeatureFlag( + FeatureFlag.PM19315EndUserActivationMvp, + ); + + // The extension page isn't applicable for mobile users, do not redirect them. + // Include before any other checks to avoid unnecessary processing. + if (!endUserFeatureEnabled || isMobile) { + return true; + } + + const currentAcct = await firstValueFrom(accountService.activeAccount$); + + if (!currentAcct) { + return router.createUrlTree(["/login"]); + } + + const hasExtensionInstalledPromise = firstValueFrom( + webBrowserInteractionService.extensionInstalled$, + ); + + const dismissedExtensionPage = await firstValueFrom( + stateProvider + .getUser(currentAcct.id, SETUP_EXTENSION_DISMISSED) + .state$.pipe(map((dismissed) => dismissed ?? false)), + ); + + const isProfileOlderThan30Days = await profileIsOlderThan30Days( + vaultProfileService, + currentAcct.id, + ).catch( + () => + // If the call for the profile fails for any reason, do not block the user + true, + ); + + if (dismissedExtensionPage || isProfileOlderThan30Days) { + return true; + } + + // Checking for the extension is a more expensive operation, do it last to avoid unnecessary delays. + const hasExtensionInstalled = await hasExtensionInstalledPromise; + + if (hasExtensionInstalled) { + return true; + } + + return router.createUrlTree(["/setup-extension"]); +}; + +/** Returns true when the user's profile is older than 30 days */ +async function profileIsOlderThan30Days( + vaultProfileService: VaultProfileService, + userId: string, +): Promise { + const creationDate = await vaultProfileService.getProfileCreationDate(userId); + return isMoreThan30DaysAgo(creationDate); +} + +/** Returns the true when the date given is older than 30 days */ +function isMoreThan30DaysAgo(date?: string | Date): boolean { + if (!date) { + return false; + } + + const inputDate = new Date(date).getTime(); + const today = new Date().getTime(); + + const differenceInMS = today - inputDate; + const msInADay = 1000 * 60 * 60 * 24; + const differenceInDays = Math.round(differenceInMS / msInADay); + + return differenceInDays > 30; +} diff --git a/apps/web/src/app/vault/services/web-browser-interaction.service.spec.ts b/apps/web/src/app/vault/services/web-browser-interaction.service.spec.ts index fef5d45e8c3..bfbfb0fb676 100644 --- a/apps/web/src/app/vault/services/web-browser-interaction.service.spec.ts +++ b/apps/web/src/app/vault/services/web-browser-interaction.service.spec.ts @@ -38,7 +38,7 @@ describe("WebBrowserInteractionService", () => { expect(installed).toBe(false); }); - tick(1500); + tick(150); })); it("returns true when the extension is installed", (done) => { @@ -58,13 +58,13 @@ describe("WebBrowserInteractionService", () => { }); // initial timeout, should emit false - tick(1500); + tick(26); expect(results[0]).toBe(false); tick(2500); // then emit `HasBwInstalled` dispatchEvent(VaultMessages.HasBwInstalled); - tick(); + tick(26); expect(results[1]).toBe(true); })); }); diff --git a/apps/web/src/app/vault/services/web-browser-interaction.service.ts b/apps/web/src/app/vault/services/web-browser-interaction.service.ts index f1005ef6dc9..1f91942591b 100644 --- a/apps/web/src/app/vault/services/web-browser-interaction.service.ts +++ b/apps/web/src/app/vault/services/web-browser-interaction.service.ts @@ -21,10 +21,19 @@ import { ExtensionPageUrls } from "@bitwarden/common/vault/enums"; import { VaultMessages } from "@bitwarden/common/vault/enums/vault-messages.enum"; /** - * The amount of time in milliseconds to wait for a response from the browser extension. + * The amount of time in milliseconds to wait for a response from the browser extension. A longer duration is + * used to allow for the extension to open and then emit to the message. * NOTE: This value isn't computed by any means, it is just a reasonable timeout for the extension to respond. */ -const MESSAGE_RESPONSE_TIMEOUT_MS = 1500; +const OPEN_RESPONSE_TIMEOUT_MS = 1500; + +/** + * Timeout for checking if the extension is installed. + * + * A shorter timeout is used to avoid waiting for too long for the extension. The listener for + * checking the installation runs in the background scripts so the response should be relatively quick. + */ +const CHECK_FOR_EXTENSION_TIMEOUT_MS = 25; @Injectable({ providedIn: "root", @@ -63,7 +72,7 @@ export class WebBrowserInteractionService { filter((event) => event.data.command === VaultMessages.PopupOpened), map(() => true), ), - timer(MESSAGE_RESPONSE_TIMEOUT_MS).pipe(map(() => false)), + timer(OPEN_RESPONSE_TIMEOUT_MS).pipe(map(() => false)), ) .pipe(take(1)) .subscribe((didOpen) => { @@ -85,7 +94,7 @@ export class WebBrowserInteractionService { filter((event) => event.data.command === VaultMessages.HasBwInstalled), map(() => true), ), - timer(MESSAGE_RESPONSE_TIMEOUT_MS).pipe(map(() => false)), + timer(CHECK_FOR_EXTENSION_TIMEOUT_MS).pipe(map(() => false)), ).pipe( tap({ subscribe: () => { diff --git a/libs/auth/src/angular/registration/registration-finish/default-registration-finish.service.ts b/libs/auth/src/angular/registration/registration-finish/default-registration-finish.service.ts index b51f45f1b27..2bef5670ac3 100644 --- a/libs/auth/src/angular/registration/registration-finish/default-registration-finish.service.ts +++ b/libs/auth/src/angular/registration/registration-finish/default-registration-finish.service.ts @@ -28,10 +28,6 @@ export class DefaultRegistrationFinishService implements RegistrationFinishServi return null; } - determineLoginSuccessRoute(): Promise { - return Promise.resolve("/vault"); - } - async finishRegistration( email: string, passwordInputResult: PasswordInputResult, diff --git a/libs/auth/src/angular/registration/registration-finish/registration-finish.component.ts b/libs/auth/src/angular/registration/registration-finish/registration-finish.component.ts index 1d1a2d8f892..dac62f039ee 100644 --- a/libs/auth/src/angular/registration/registration-finish/registration-finish.component.ts +++ b/libs/auth/src/angular/registration/registration-finish/registration-finish.component.ts @@ -204,8 +204,7 @@ export class RegistrationFinishComponent implements OnInit, OnDestroy { await this.loginSuccessHandlerService.run(authenticationResult.userId); - const successRoute = await this.registrationFinishService.determineLoginSuccessRoute(); - await this.router.navigate([successRoute]); + await this.router.navigate(["/vault"]); } catch (e) { // If login errors, redirect to login page per product. Don't show error this.logService.error("Error logging in after registration: ", e.message); diff --git a/libs/auth/src/angular/registration/registration-finish/registration-finish.service.ts b/libs/auth/src/angular/registration/registration-finish/registration-finish.service.ts index 523a4c79c54..5f3c04e5155 100644 --- a/libs/auth/src/angular/registration/registration-finish/registration-finish.service.ts +++ b/libs/auth/src/angular/registration/registration-finish/registration-finish.service.ts @@ -16,11 +16,6 @@ export abstract class RegistrationFinishService { */ abstract getMasterPasswordPolicyOptsFromOrgInvite(): Promise; - /** - * Returns the route the user is redirected to after a successful login. - */ - abstract determineLoginSuccessRoute(): Promise; - /** * Finishes the registration process by creating a new user account. * diff --git a/libs/common/src/platform/state/state-definitions.ts b/libs/common/src/platform/state/state-definitions.ts index 93c489a343e..a1c3ee35c5c 100644 --- a/libs/common/src/platform/state/state-definitions.ts +++ b/libs/common/src/platform/state/state-definitions.ts @@ -202,6 +202,13 @@ export const SECURITY_TASKS_DISK = new StateDefinition("securityTasks", "disk"); export const AT_RISK_PASSWORDS_PAGE_DISK = new StateDefinition("atRiskPasswordsPage", "disk"); export const NOTIFICATION_DISK = new StateDefinition("notifications", "disk"); export const NUDGES_DISK = new StateDefinition("nudges", "disk", { web: "disk-local" }); +export const SETUP_EXTENSION_DISMISSED_DISK = new StateDefinition( + "setupExtensionDismissed", + "disk", + { + web: "disk-local", + }, +); export const VAULT_BROWSER_INTRO_CAROUSEL = new StateDefinition( "vaultBrowserIntroCarousel", "disk", From e8629e5e1b276973ab9820e4ddc4a7a215bc69be Mon Sep 17 00:00:00 2001 From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Date: Wed, 23 Jul 2025 14:00:07 +0100 Subject: [PATCH 17/21] Resolve the dropdown display error (#15704) --- .../src/app/billing/settings/sponsored-families.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/app/billing/settings/sponsored-families.component.html b/apps/web/src/app/billing/settings/sponsored-families.component.html index 7708f63365e..7d240cb0665 100644 --- a/apps/web/src/app/billing/settings/sponsored-families.component.html +++ b/apps/web/src/app/billing/settings/sponsored-families.component.html @@ -28,7 +28,7 @@ > Date: Wed, 23 Jul 2025 09:51:02 -0400 Subject: [PATCH 18/21] Removing the notifications feature flag and logic (#15551) --- .../critical-applications.component.html | 1 - .../access-intelligence/critical-applications.component.ts | 6 ------ libs/common/src/enums/feature-flag.enum.ts | 6 ------ 3 files changed, 13 deletions(-) diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/critical-applications.component.html b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/critical-applications.component.html index 4e2b4e5c404..ffef3f3b0b9 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/critical-applications.component.html +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/critical-applications.component.html @@ -29,7 +29,6 @@

{{ "criticalApplications" | i18n }}

+ diff --git a/libs/components/src/search/search.component.ts b/libs/components/src/search/search.component.ts index ef12e7eead6..c6c5f2757dd 100644 --- a/libs/components/src/search/search.component.ts +++ b/libs/components/src/search/search.component.ts @@ -1,6 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { Component, ElementRef, ViewChild, input, model } from "@angular/core"; +import { NgIf, NgClass } from "@angular/common"; +import { Component, ElementRef, ViewChild, input, model, signal, computed } from "@angular/core"; import { ControlValueAccessor, NG_VALUE_ACCESSOR, @@ -16,6 +17,9 @@ import { FocusableElement } from "../shared/focusable-element"; let nextId = 0; +/** + * Do not nest Search components inside another `
`, as they already contain their own standalone `` element for searching. + */ @Component({ selector: "bit-search", templateUrl: "./search.component.html", @@ -30,7 +34,7 @@ let nextId = 0; useExisting: SearchComponent, }, ], - imports: [InputModule, ReactiveFormsModule, FormsModule, I18nPipe], + imports: [InputModule, ReactiveFormsModule, FormsModule, I18nPipe, NgIf, NgClass], }) export class SearchComponent implements ControlValueAccessor, FocusableElement { private notifyOnChange: (v: string) => void; @@ -43,6 +47,11 @@ export class SearchComponent implements ControlValueAccessor, FocusableElement { // Use `type="text"` for Safari to improve rendering performance protected inputType = isBrowserSafariApi() ? ("text" as const) : ("search" as const); + protected isInputFocused = signal(false); + protected isFormHovered = signal(false); + + protected showResetButton = computed(() => this.isInputFocused() || this.isFormHovered()); + readonly disabled = model(); readonly placeholder = input(); readonly autocomplete = input(); @@ -52,11 +61,20 @@ export class SearchComponent implements ControlValueAccessor, FocusableElement { } onChange(searchText: string) { + this.searchText = searchText; // update the model when the input changes (so we can use it with *ngIf in the template) if (this.notifyOnChange != undefined) { this.notifyOnChange(searchText); } } + // Handle the reset button click + clearSearch() { + this.searchText = ""; + if (this.notifyOnChange) { + this.notifyOnChange(""); + } + } + onTouch() { if (this.notifyOnTouch != undefined) { this.notifyOnTouch(); diff --git a/libs/components/src/search/search.mdx b/libs/components/src/search/search.mdx index 7775225b8c2..98e91162c94 100644 --- a/libs/components/src/search/search.mdx +++ b/libs/components/src/search/search.mdx @@ -1,4 +1,4 @@ -import { Meta, Canvas, Source, Primary, Controls, Title } from "@storybook/addon-docs"; +import { Meta, Canvas, Source, Primary, Controls, Title, Description } from "@storybook/addon-docs"; import * as stories from "./search.stories"; @@ -9,6 +9,7 @@ import { SearchModule } from "@bitwarden/components"; ``` Search field +