1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-18 17:23:37 +00:00

[PM-16937] Remove Billing Circular Dependency (#13085)

* Remove circular dependency between billing services and components

* Removed `logService` from `billing-api.service.ts`

* Resolved failed test

* Removed @bitwarden/ui-common

* Added optional `title` parameter to `BillingNotificationService` functions

* Removed @bitwarden/platform from libs/common/tsconfig.json

* Update apps/web/src/app/billing/services/billing-notification.service.spec.ts

Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com>

* Update apps/web/src/app/billing/services/billing-notification.service.spec.ts

Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com>

* Resolved build errors

* Resolved issue where free trial banner wouldn't display if missing a payment method

---------

Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com>
This commit is contained in:
Conner Turnbull
2025-03-11 13:43:19 -04:00
committed by GitHub
parent 18ad710909
commit 00e822fb13
15 changed files with 327 additions and 168 deletions

View File

@@ -8,6 +8,7 @@ import { map } from "rxjs";
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions";
import { InvoiceResponse } from "@bitwarden/common/billing/models/response/invoices.response";
import { BillingNotificationService } from "@bitwarden/web-vault/app/billing/services/billing-notification.service";
@Component({
templateUrl: "./provider-billing-history.component.html",
@@ -19,6 +20,7 @@ export class ProviderBillingHistoryComponent {
private activatedRoute: ActivatedRoute,
private billingApiService: BillingApiServiceAbstraction,
private datePipe: DatePipe,
private billingNotificationService: BillingNotificationService,
) {
this.activatedRoute.params
.pipe(
@@ -30,13 +32,27 @@ export class ProviderBillingHistoryComponent {
.subscribe();
}
getClientInvoiceReport = (invoiceId: string) =>
this.billingApiService.getProviderClientInvoiceReport(this.providerId, invoiceId);
getClientInvoiceReport = async (invoiceId: string) => {
try {
return await this.billingApiService.getProviderClientInvoiceReport(
this.providerId,
invoiceId,
);
} catch (error) {
this.billingNotificationService.handleError(error);
}
};
getClientInvoiceReportName = (invoice: InvoiceResponse) => {
const date = this.datePipe.transform(invoice.date, "yyyyMMdd");
return `bitwarden_provider-billing-history_${date}_${invoice.number}`;
};
getInvoices = async () => await this.billingApiService.getProviderInvoices(this.providerId);
getInvoices = async () => {
try {
return await this.billingApiService.getProviderInvoices(this.providerId);
} catch (error) {
this.billingNotificationService.handleError(error);
}
};
}

View File

@@ -11,7 +11,8 @@ import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstract
import { UpdateClientOrganizationRequest } from "@bitwarden/common/billing/models/request/update-client-organization.request";
import { ProviderPlanResponse } from "@bitwarden/common/billing/models/response/provider-subscription-response";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { DialogService, ToastService } from "@bitwarden/components";
import { DialogService } from "@bitwarden/components";
import { BillingNotificationService } from "@bitwarden/web-vault/app/billing/services/billing-notification.service";
type ManageClientSubscriptionDialogParams = {
organization: ProviderOrganizationOrganizationDetailsResponse;
@@ -56,30 +57,34 @@ export class ManageClientSubscriptionDialogComponent implements OnInit {
@Inject(DIALOG_DATA) protected dialogParams: ManageClientSubscriptionDialogParams,
private dialogRef: DialogRef<ManageClientSubscriptionDialogResultType>,
private i18nService: I18nService,
private toastService: ToastService,
private billingNotificationService: BillingNotificationService,
) {}
async ngOnInit(): Promise<void> {
const response = await this.billingApiService.getProviderSubscription(
this.dialogParams.provider.id,
);
try {
const response = await this.billingApiService.getProviderSubscription(
this.dialogParams.provider.id,
);
this.providerPlan = response.plans.find(
(plan) => plan.planName === this.dialogParams.organization.plan,
);
this.providerPlan = response.plans.find(
(plan) => plan.planName === this.dialogParams.organization.plan,
);
this.assignedSeats = this.providerPlan.assignedSeats;
this.openSeats = this.providerPlan.seatMinimum - this.providerPlan.assignedSeats;
this.purchasedSeats = this.providerPlan.purchasedSeats;
this.seatMinimum = this.providerPlan.seatMinimum;
this.assignedSeats = this.providerPlan.assignedSeats;
this.openSeats = this.providerPlan.seatMinimum - this.providerPlan.assignedSeats;
this.purchasedSeats = this.providerPlan.purchasedSeats;
this.seatMinimum = this.providerPlan.seatMinimum;
this.formGroup.controls.assignedSeats.addValidators(
this.isServiceUserWithPurchasedSeats
? this.createPurchasedSeatsValidator()
: this.createUnassignedSeatsValidator(),
);
this.loading = false;
this.formGroup.controls.assignedSeats.addValidators(
this.isServiceUserWithPurchasedSeats
? this.createPurchasedSeatsValidator()
: this.createUnassignedSeatsValidator(),
);
} catch (error) {
this.billingNotificationService.handleError(error);
} finally {
this.loading = false;
}
}
submit = async () => {
@@ -91,24 +96,25 @@ export class ManageClientSubscriptionDialogComponent implements OnInit {
return;
}
const request = new UpdateClientOrganizationRequest();
request.assignedSeats = this.formGroup.value.assignedSeats;
request.name = this.dialogParams.organization.organizationName;
try {
const request = new UpdateClientOrganizationRequest();
request.assignedSeats = this.formGroup.value.assignedSeats;
request.name = this.dialogParams.organization.organizationName;
await this.billingApiService.updateProviderClientOrganization(
this.dialogParams.provider.id,
this.dialogParams.organization.id,
request,
);
await this.billingApiService.updateProviderClientOrganization(
this.dialogParams.provider.id,
this.dialogParams.organization.id,
request,
);
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("subscriptionUpdated"),
});
this.billingNotificationService.showSuccess(this.i18nService.t("subscriptionUpdated"));
this.loading = false;
this.dialogRef.close(this.ResultType.Submitted);
this.dialogRef.close(this.ResultType.Submitted);
} catch (error) {
this.billingNotificationService.handleError(error);
} finally {
this.loading = false;
}
};
createPurchasedSeatsValidator =

View File

@@ -23,6 +23,7 @@ import {
ToastService,
} from "@bitwarden/components";
import { SharedOrganizationModule } from "@bitwarden/web-vault/app/admin-console/organizations/shared";
import { BillingNotificationService } from "@bitwarden/web-vault/app/billing/services/billing-notification.service";
import { HeaderModule } from "@bitwarden/web-vault/app/layouts/header/header.module";
import { WebProviderService } from "../../../admin-console/providers/services/web-provider.service";
@@ -83,6 +84,7 @@ export class ManageClientsComponent {
private validationService: ValidationService,
private webProviderService: WebProviderService,
private configService: ConfigService,
private billingNotificationService: BillingNotificationService,
) {
this.activatedRoute.queryParams.pipe(first(), takeUntilDestroyed()).subscribe((queryParams) => {
this.searchControl.setValue(queryParams.search);
@@ -120,13 +122,17 @@ export class ManageClientsComponent {
}
async load() {
this.provider = await firstValueFrom(this.providerService.get$(this.providerId));
this.isProviderAdmin = this.provider?.type === ProviderUserType.ProviderAdmin;
this.dataSource.data = (
await this.billingApiService.getProviderClientOrganizations(this.providerId)
).data;
this.plans = (await this.billingApiService.getPlans()).data;
this.loading = false;
try {
this.provider = await firstValueFrom(this.providerService.get$(this.providerId));
this.isProviderAdmin = this.provider?.type === ProviderUserType.ProviderAdmin;
this.dataSource.data = (
await this.billingApiService.getProviderClientOrganizations(this.providerId)
).data;
this.plans = (await this.billingApiService.getPlans()).data;
this.loading = false;
} catch (error) {
this.billingNotificationService.handleError(error);
}
}
addExistingOrganization = async () => {

View File

@@ -12,7 +12,7 @@ import {
ProviderSubscriptionResponse,
} from "@bitwarden/common/billing/models/response/provider-subscription-response";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { ToastService } from "@bitwarden/components";
import { BillingNotificationService } from "@bitwarden/web-vault/app/billing/services/billing-notification.service";
@Component({
selector: "app-provider-subscription",
@@ -33,7 +33,7 @@ export class ProviderSubscriptionComponent implements OnInit, OnDestroy {
private billingApiService: BillingApiServiceAbstraction,
private i18nService: I18nService,
private route: ActivatedRoute,
private toastService: ToastService,
private billingNotificationService: BillingNotificationService,
) {}
async ngOnInit() {
@@ -54,20 +54,26 @@ export class ProviderSubscriptionComponent implements OnInit, OnDestroy {
return;
}
this.loading = true;
this.subscription = await this.billingApiService.getProviderSubscription(this.providerId);
this.totalCost =
((100 - this.subscription.discountPercentage) / 100) * this.sumCost(this.subscription.plans);
this.loading = false;
try {
this.subscription = await this.billingApiService.getProviderSubscription(this.providerId);
this.totalCost =
((100 - this.subscription.discountPercentage) / 100) *
this.sumCost(this.subscription.plans);
} catch (error) {
this.billingNotificationService.handleError(error);
} finally {
this.loading = false;
}
}
protected updateTaxInformation = async (taxInformation: TaxInformation) => {
const request = ExpandedTaxInfoUpdateRequest.From(taxInformation);
await this.billingApiService.updateProviderTaxInformation(this.providerId, request);
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("updatedTaxInformation"),
});
try {
const request = ExpandedTaxInfoUpdateRequest.From(taxInformation);
await this.billingApiService.updateProviderTaxInformation(this.providerId, request);
this.billingNotificationService.showSuccess(this.i18nService.t("updatedTaxInformation"));
} catch (error) {
this.billingNotificationService.handleError(error);
}
};
protected getFormattedCost(

View File

@@ -16,6 +16,8 @@ import {
firstValueFrom,
of,
filter,
catchError,
from,
} from "rxjs";
import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe";
@@ -32,6 +34,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { DialogService } from "@bitwarden/components";
import { BillingNotificationService } from "@bitwarden/web-vault/app/billing/services/billing-notification.service";
import { TrialFlowService } from "@bitwarden/web-vault/app/billing/services/trial-flow.service";
import { FreeTrial } from "@bitwarden/web-vault/app/billing/types/free-trial";
@@ -126,6 +129,7 @@ export class OverviewComponent implements OnInit, OnDestroy {
private organizationApiService: OrganizationApiServiceAbstraction,
private trialFlowService: TrialFlowService,
private organizationBillingService: OrganizationBillingServiceAbstraction,
private billingNotificationService: BillingNotificationService,
) {}
ngOnInit() {
@@ -161,12 +165,18 @@ export class OverviewComponent implements OnInit, OnDestroy {
combineLatest([
of(org),
this.organizationApiService.getSubscription(org.id),
this.organizationBillingService.getPaymentSource(org.id),
from(this.organizationBillingService.getPaymentSource(org.id)).pipe(
catchError((error: unknown) => {
this.billingNotificationService.handleError(error);
return of(null);
}),
),
]),
),
map(([org, sub, paymentSource]) => {
return this.trialFlowService.checkForOrgsWithUpcomingPaymentIssues(org, sub, paymentSource);
}),
filter((result) => result !== null),
takeUntil(this.destroy$),
);