diff --git a/apps/web/src/app/billing/organizations/change-plan-dialog.component.ts b/apps/web/src/app/billing/organizations/change-plan-dialog.component.ts index f8ccd96e97a..39d3e4cf2c5 100644 --- a/apps/web/src/app/billing/organizations/change-plan-dialog.component.ts +++ b/apps/web/src/app/billing/organizations/change-plan-dialog.component.ts @@ -58,6 +58,7 @@ import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.serv import { DialogService, ToastService } from "@bitwarden/components"; import { KeyService } from "@bitwarden/key-management"; +import { BillingNotificationService } from "../services/billing-notification.service"; import { BillingSharedModule } from "../shared/billing-shared.module"; import { PaymentComponent } from "../shared/payment/payment.component"; @@ -208,6 +209,7 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { private taxService: TaxServiceAbstraction, private accountService: AccountService, private organizationBillingService: OrganizationBillingService, + private billingNotificationService: BillingNotificationService, ) {} async ngOnInit(): Promise { @@ -228,10 +230,14 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { .organizations$(userId) .pipe(getOrganizationById(this.organizationId)), ); - const { accountCredit, paymentSource } = - await this.billingApiService.getOrganizationPaymentMethod(this.organizationId); - this.accountCredit = accountCredit; - this.paymentSource = paymentSource; + try { + const { accountCredit, paymentSource } = + await this.billingApiService.getOrganizationPaymentMethod(this.organizationId); + this.accountCredit = accountCredit; + this.paymentSource = paymentSource; + } catch (error) { + this.billingNotificationService.handleError(error); + } } if (!this.selfHosted) { 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 a8b2c7a46f1..a13913dabf3 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 @@ -23,6 +23,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { SyncService } from "@bitwarden/common/platform/sync"; import { DialogService, ToastService } from "@bitwarden/components"; +import { BillingNotificationService } from "../../services/billing-notification.service"; import { TrialFlowService } from "../../services/trial-flow.service"; import { AddCreditDialogResult, @@ -66,6 +67,7 @@ export class OrganizationPaymentMethodComponent implements OnDestroy { private organizationService: OrganizationService, private accountService: AccountService, protected syncService: SyncService, + private billingNotificationService: BillingNotificationService, ) { this.activatedRoute.params .pipe( @@ -115,47 +117,52 @@ export class OrganizationPaymentMethodComponent implements OnDestroy { protected load = async (): Promise => { this.loading = true; - const { accountCredit, paymentSource, subscriptionStatus } = - await this.billingApiService.getOrganizationPaymentMethod(this.organizationId); - this.accountCredit = accountCredit; - this.paymentSource = paymentSource; - this.subscriptionStatus = subscriptionStatus; - - if (this.organizationId) { - const organizationSubscriptionPromise = this.organizationApiService.getSubscription( - this.organizationId, - ); - const userId = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((a) => a?.id)), - ); - const organizationPromise = await firstValueFrom( - this.organizationService - .organizations$(userId) - .pipe(getOrganizationById(this.organizationId)), - ); - - [this.organizationSubscriptionResponse, this.organization] = await Promise.all([ - organizationSubscriptionPromise, - organizationPromise, - ]); - this.freeTrialData = this.trialFlowService.checkForOrgsWithUpcomingPaymentIssues( - this.organization, - this.organizationSubscriptionResponse, - paymentSource, - ); - } - this.isUnpaid = this.subscriptionStatus === "unpaid" ?? false; - // If the flag `launchPaymentModalAutomatically` is set to true, - // we schedule a timeout (delay of 800ms) to automatically launch the payment modal. - // This delay ensures that any prior UI/rendering operations complete before triggering the modal. - if (this.launchPaymentModalAutomatically) { - window.setTimeout(async () => { - await this.changePayment(); - this.launchPaymentModalAutomatically = false; - this.location.replaceState(this.location.path(), "", {}); - }, 800); + try { + const { accountCredit, paymentSource, subscriptionStatus } = + await this.billingApiService.getOrganizationPaymentMethod(this.organizationId); + this.accountCredit = accountCredit; + this.paymentSource = paymentSource; + this.subscriptionStatus = subscriptionStatus; + + if (this.organizationId) { + const organizationSubscriptionPromise = this.organizationApiService.getSubscription( + this.organizationId, + ); + const userId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); + const organizationPromise = await firstValueFrom( + this.organizationService + .organizations$(userId) + .pipe(getOrganizationById(this.organizationId)), + ); + + [this.organizationSubscriptionResponse, this.organization] = await Promise.all([ + organizationSubscriptionPromise, + organizationPromise, + ]); + this.freeTrialData = this.trialFlowService.checkForOrgsWithUpcomingPaymentIssues( + this.organization, + this.organizationSubscriptionResponse, + paymentSource, + ); + } + this.isUnpaid = this.subscriptionStatus === "unpaid" ?? false; + // If the flag `launchPaymentModalAutomatically` is set to true, + // we schedule a timeout (delay of 800ms) to automatically launch the payment modal. + // This delay ensures that any prior UI/rendering operations complete before triggering the modal. + if (this.launchPaymentModalAutomatically) { + window.setTimeout(async () => { + await this.changePayment(); + this.launchPaymentModalAutomatically = false; + this.location.replaceState(this.location.path(), "", {}); + }, 800); + } + } catch (error) { + this.billingNotificationService.handleError(error); + } finally { + this.loading = false; } - this.loading = false; }; protected updatePaymentMethod = async (): Promise => { diff --git a/apps/web/src/app/billing/services/billing-notification.service.spec.ts b/apps/web/src/app/billing/services/billing-notification.service.spec.ts new file mode 100644 index 00000000000..d3a4d461574 --- /dev/null +++ b/apps/web/src/app/billing/services/billing-notification.service.spec.ts @@ -0,0 +1,76 @@ +import { mock, MockProxy } from "jest-mock-extended"; + +import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { ToastService } from "@bitwarden/components"; + +import { BillingNotificationService } from "./billing-notification.service"; + +describe("BillingNotificationService", () => { + let service: BillingNotificationService; + let logService: MockProxy; + let toastService: MockProxy; + + beforeEach(() => { + logService = mock(); + toastService = mock(); + service = new BillingNotificationService(logService, toastService); + }); + + describe("handleError", () => { + it("should log error and show toast for ErrorResponse", () => { + const error = new ErrorResponse(["test error"], 400); + + expect(() => service.handleError(error)).toThrow(); + expect(logService.error).toHaveBeenCalledWith(error); + expect(toastService.showToast).toHaveBeenCalledWith({ + variant: "error", + title: "", + message: error.getSingleMessage(), + }); + }); + + it("shows error toast with the provided error", () => { + const error = new ErrorResponse(["test error"], 400); + + expect(() => service.handleError(error, "Test Title")).toThrow(); + expect(toastService.showToast).toHaveBeenCalledWith({ + variant: "error", + title: "Test Title", + message: error.getSingleMessage(), + }); + }); + + it("should only log error for non-ErrorResponse", () => { + const error = new Error("test error"); + + expect(() => service.handleError(error)).toThrow(); + expect(logService.error).toHaveBeenCalledWith(error); + expect(toastService.showToast).not.toHaveBeenCalled(); + }); + }); + + describe("showSuccess", () => { + it("shows success toast with default title when provided title is empty", () => { + const message = "test message"; + service.showSuccess(message); + + expect(toastService.showToast).toHaveBeenCalledWith({ + variant: "success", + title: "", + message, + }); + }); + + it("should show success toast with custom title", () => { + const message = "test message"; + service.showSuccess(message, "Success Title"); + + expect(toastService.showToast).toHaveBeenCalledWith({ + variant: "success", + title: "Success Title", + message, + }); + }); + }); +}); diff --git a/apps/web/src/app/billing/services/billing-notification.service.ts b/apps/web/src/app/billing/services/billing-notification.service.ts new file mode 100644 index 00000000000..6695e516ca8 --- /dev/null +++ b/apps/web/src/app/billing/services/billing-notification.service.ts @@ -0,0 +1,35 @@ +import { Injectable } from "@angular/core"; + +import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { ToastService } from "@bitwarden/components"; + +@Injectable({ + providedIn: "root", +}) +export class BillingNotificationService { + constructor( + private logService: LogService, + private toastService: ToastService, + ) {} + + handleError(error: unknown, title: string = "") { + this.logService.error(error); + if (error instanceof ErrorResponse) { + this.toastService.showToast({ + variant: "error", + title: title, + message: error.getSingleMessage(), + }); + } + throw error; + } + + showSuccess(message: string, title: string = "") { + this.toastService.showToast({ + variant: "success", + title: title, + message: message, + }); + } +} diff --git a/apps/web/src/app/billing/services/billing-services.module.ts b/apps/web/src/app/billing/services/billing-services.module.ts index 7412d47c79c..56b906cdaae 100644 --- a/apps/web/src/app/billing/services/billing-services.module.ts +++ b/apps/web/src/app/billing/services/billing-services.module.ts @@ -1,4 +1,8 @@ import { NgModule } from "@angular/core"; -@NgModule({}) +import { BillingNotificationService } from "./billing-notification.service"; + +@NgModule({ + providers: [BillingNotificationService], +}) export class BillingServicesModule {} diff --git a/apps/web/src/app/vault/individual-vault/vault.component.ts b/apps/web/src/app/vault/individual-vault/vault.component.ts index ff8a008bcc5..88de4d57ff1 100644 --- a/apps/web/src/app/vault/individual-vault/vault.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault.component.ts @@ -24,6 +24,7 @@ import { take, takeUntil, tap, + catchError, } from "rxjs/operators"; import { @@ -74,6 +75,7 @@ import { PasswordRepromptService, } from "@bitwarden/vault"; +import { BillingNotificationService } from "../../billing/services/billing-notification.service"; import { TrialFlowService } from "../../billing/services/trial-flow.service"; import { FreeTrial } from "../../billing/types/free-trial"; import { SharedModule } from "../../shared/shared.module"; @@ -213,9 +215,17 @@ export class VaultComponent implements OnInit, OnDestroy { ownerOrgs.map((org) => combineLatest([ 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); + }), + ), ]).pipe( map(([subscription, paymentSource]) => { + if (!paymentSource) { + return null; + } return this.trialFlowService.checkForOrgsWithUpcomingPaymentIssues( org, subscription, @@ -226,7 +236,7 @@ export class VaultComponent implements OnInit, OnDestroy { ), ); }), - map((results) => results.filter((result) => result.shownBanner)), + map((results) => results.filter((result) => result !== null && result.shownBanner)), shareReplay({ refCount: false, bufferSize: 1 }), ); @@ -262,6 +272,7 @@ export class VaultComponent implements OnInit, OnDestroy { protected billingApiService: BillingApiServiceAbstraction, private trialFlowService: TrialFlowService, private organizationBillingService: OrganizationBillingServiceAbstraction, + private billingNotificationService: BillingNotificationService, ) {} async ngOnInit() { diff --git a/apps/web/src/app/vault/org-vault/vault.component.ts b/apps/web/src/app/vault/org-vault/vault.component.ts index b4ba9ff5512..7d4939b1700 100644 --- a/apps/web/src/app/vault/org-vault/vault.component.ts +++ b/apps/web/src/app/vault/org-vault/vault.component.ts @@ -24,6 +24,7 @@ import { switchMap, takeUntil, tap, + catchError, } from "rxjs/operators"; import { @@ -77,6 +78,7 @@ import { import { GroupApiService, GroupView } from "../../admin-console/organizations/core"; import { openEntityEventsDialog } from "../../admin-console/organizations/manage/entity-events.component"; +import { BillingNotificationService } from "../../billing/services/billing-notification.service"; import { ResellerWarning, ResellerWarningService, @@ -254,6 +256,7 @@ export class VaultComponent implements OnInit, OnDestroy { private organizationBillingService: OrganizationBillingServiceAbstraction, private resellerWarningService: ResellerWarningService, private accountService: AccountService, + private billingNotificationService: BillingNotificationService, ) {} async ngOnInit() { @@ -627,12 +630,21 @@ export class VaultComponent 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]) => { + if (!paymentSource) { + return null; + } return this.trialFlowService.checkForOrgsWithUpcomingPaymentIssues(org, sub, paymentSource); }), + filter((result) => result !== null), ); this.resellerWarning$ = organization$.pipe( diff --git a/bitwarden_license/bit-web/src/app/billing/providers/billing-history/provider-billing-history.component.ts b/bitwarden_license/bit-web/src/app/billing/providers/billing-history/provider-billing-history.component.ts index cf45d404c01..fa3a617b45f 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/billing-history/provider-billing-history.component.ts +++ b/bitwarden_license/bit-web/src/app/billing/providers/billing-history/provider-billing-history.component.ts @@ -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); + } + }; } diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-client-subscription-dialog.component.ts b/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-client-subscription-dialog.component.ts index ee0e5c07c68..e5da22a861e 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-client-subscription-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-client-subscription-dialog.component.ts @@ -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, private i18nService: I18nService, - private toastService: ToastService, + private billingNotificationService: BillingNotificationService, ) {} async ngOnInit(): Promise { - const response = await this.billingApiService.getProviderSubscription( - this.dialogParams.provider.id, - ); - - 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.formGroup.controls.assignedSeats.addValidators( - this.isServiceUserWithPurchasedSeats - ? this.createPurchasedSeatsValidator() - : this.createUnassignedSeatsValidator(), - ); - - this.loading = false; + try { + const response = await this.billingApiService.getProviderSubscription( + this.dialogParams.provider.id, + ); + + 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.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 = diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-clients.component.ts b/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-clients.component.ts index 07434369122..c0b4ba72499 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-clients.component.ts +++ b/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-clients.component.ts @@ -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 () => { diff --git a/bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription.component.ts b/bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription.component.ts index 02fa29578d9..df8a85e3e42 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription.component.ts +++ b/bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription.component.ts @@ -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( diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/overview/overview.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/overview/overview.component.ts index 7eb28b2bc2d..698448c2d06 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/overview/overview.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/overview/overview.component.ts @@ -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$), ); diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 719e3a084f1..6d19e7d594f 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -1271,7 +1271,7 @@ const safeProviders: SafeProvider[] = [ safeProvider({ provide: BillingApiServiceAbstraction, useClass: BillingApiService, - deps: [ApiServiceAbstraction, LogService, ToastService], + deps: [ApiServiceAbstraction], }), safeProvider({ provide: TaxServiceAbstraction, diff --git a/libs/common/src/billing/services/billing-api.service.ts b/libs/common/src/billing/services/billing-api.service.ts index 4306324395e..e7552b24d24 100644 --- a/libs/common/src/billing/services/billing-api.service.ts +++ b/libs/common/src/billing/services/billing-api.service.ts @@ -1,13 +1,10 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { ToastService } from "@bitwarden/components"; import { ApiService } from "../../abstractions/api.service"; import { OrganizationCreateRequest } from "../../admin-console/models/request/organization-create.request"; import { ProviderOrganizationOrganizationDetailsResponse } from "../../admin-console/models/response/provider/provider-organization.response"; -import { ErrorResponse } from "../../models/response/error.response"; import { ListResponse } from "../../models/response/list.response"; -import { LogService } from "../../platform/abstractions/log.service"; import { BillingApiServiceAbstraction } from "../abstractions"; import { PaymentMethodType } from "../enums"; import { CreateClientOrganizationRequest } from "../models/request/create-client-organization.request"; @@ -23,11 +20,7 @@ import { PlanResponse } from "../models/response/plan.response"; import { ProviderSubscriptionResponse } from "../models/response/provider-subscription-response"; export class BillingApiService implements BillingApiServiceAbstraction { - constructor( - private apiService: ApiService, - private logService: LogService, - private toastService: ToastService, - ) {} + constructor(private apiService: ApiService) {} cancelOrganizationSubscription( organizationId: string, @@ -89,14 +82,12 @@ export class BillingApiService implements BillingApiServiceAbstraction { } async getOrganizationPaymentMethod(organizationId: string): Promise { - const response = await this.execute(() => - this.apiService.send( - "GET", - "/organizations/" + organizationId + "/billing/payment-method", - null, - true, - true, - ), + const response = await this.apiService.send( + "GET", + "/organizations/" + organizationId + "/billing/payment-method", + null, + true, + true, ); return new PaymentMethodResponse(response); } @@ -120,34 +111,34 @@ export class BillingApiService implements BillingApiServiceAbstraction { async getProviderClientOrganizations( providerId: string, ): Promise> { - const response = await this.execute(() => - this.apiService.send("GET", "/providers/" + providerId + "/organizations", null, true, true), + const response = await this.apiService.send( + "GET", + "/providers/" + providerId + "/organizations", + null, + true, + true, ); return new ListResponse(response, ProviderOrganizationOrganizationDetailsResponse); } async getProviderInvoices(providerId: string): Promise { - const response = await this.execute(() => - this.apiService.send( - "GET", - "/providers/" + providerId + "/billing/invoices", - null, - true, - true, - ), + const response = await this.apiService.send( + "GET", + "/providers/" + providerId + "/billing/invoices", + null, + true, + true, ); return new InvoicesResponse(response); } async getProviderSubscription(providerId: string): Promise { - const response = await this.execute(() => - this.apiService.send( - "GET", - "/providers/" + providerId + "/billing/subscription", - null, - true, - true, - ), + const response = await this.apiService.send( + "GET", + "/providers/" + providerId + "/billing/subscription", + null, + true, + true, ); return new ProviderSubscriptionResponse(response); } @@ -227,20 +218,4 @@ export class BillingApiService implements BillingApiServiceAbstraction { false, ); } - - private async execute(request: () => Promise): Promise { - try { - return await request(); - } catch (error) { - this.logService.error(error); - if (error instanceof ErrorResponse) { - this.toastService.showToast({ - variant: "error", - title: null, - message: error.getSingleMessage(), - }); - } - throw error; - } - } } diff --git a/libs/common/tsconfig.json b/libs/common/tsconfig.json index 2d1379f9c5f..4edbc8ca43a 100644 --- a/libs/common/tsconfig.json +++ b/libs/common/tsconfig.json @@ -6,12 +6,7 @@ "@bitwarden/auth/common": ["../auth/src/common"], // TODO: Remove once circular dependencies in admin-console, auth and key-management are resolved "@bitwarden/common/*": ["../common/src/*"], - // TODO: Remove once billing stops depending on components - "@bitwarden/components": ["../components/src"], - "@bitwarden/key-management": ["../key-management/src"], - "@bitwarden/platform": ["../platform/src"], - // TODO: Remove once billing stops depending on components - "@bitwarden/ui-common": ["../ui/common/src"] + "@bitwarden/key-management": ["../key-management/src"] } }, "include": ["src", "spec", "./custom-matchers.d.ts", "../key-management/src/index.ts"],