diff --git a/apps/web/src/app/billing/individual/billing-history-view.component.html b/apps/web/src/app/billing/individual/billing-history-view.component.html index 2491fc42c7..7dbd8d1792 100644 --- a/apps/web/src/app/billing/individual/billing-history-view.component.html +++ b/apps/web/src/app/billing/individual/billing-history-view.component.html @@ -2,17 +2,6 @@

{{ "billingHistory" | i18n }}

- {{ "loading" | i18n }} - - + + + diff --git a/apps/web/src/app/billing/individual/billing-history-view.component.ts b/apps/web/src/app/billing/individual/billing-history-view.component.ts index 3ee6415d6d..e88d9bb154 100644 --- a/apps/web/src/app/billing/individual/billing-history-view.component.ts +++ b/apps/web/src/app/billing/individual/billing-history-view.component.ts @@ -1,8 +1,11 @@ import { Component, OnInit } from "@angular/core"; import { Router } from "@angular/router"; -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { BillingHistoryResponse } from "@bitwarden/common/billing/models/response/billing-history.response"; +import { AccountBillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/account/account-billing-api.service.abstraction"; +import { + BillingInvoiceResponse, + BillingTransactionResponse, +} from "@bitwarden/common/billing/models/response/billing.response"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @Component({ @@ -11,12 +14,14 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl export class BillingHistoryViewComponent implements OnInit { loading = false; firstLoaded = false; - billing: BillingHistoryResponse; + invoices: BillingInvoiceResponse[] = []; + transactions: BillingTransactionResponse[] = []; + hasAdditionalHistory: boolean = false; constructor( - private apiService: ApiService, private platformUtilsService: PlatformUtilsService, private router: Router, + private accountBillingApiService: AccountBillingApiServiceAbstraction, ) {} async ngOnInit() { @@ -35,7 +40,27 @@ export class BillingHistoryViewComponent implements OnInit { return; } this.loading = true; - this.billing = await this.apiService.getUserBillingHistory(); + + const invoicesPromise = this.accountBillingApiService.getBillingInvoices( + this.invoices.length > 0 ? this.invoices[this.invoices.length - 1].id : null, + ); + + const transactionsPromise = this.accountBillingApiService.getBillingTransactions( + this.transactions.length > 0 + ? this.transactions[this.transactions.length - 1].createdDate + : null, + ); + + const accountInvoices = await invoicesPromise; + const accountTransactions = await transactionsPromise; + const pageSize = 5; + + this.invoices = [...this.invoices, ...accountInvoices]; + this.transactions = [...this.transactions, ...accountTransactions]; + this.hasAdditionalHistory = !( + accountInvoices.length < pageSize && accountTransactions.length < pageSize + ); + this.loading = false; } } diff --git a/apps/web/src/app/billing/organizations/organization-billing-history-view.component.html b/apps/web/src/app/billing/organizations/organization-billing-history-view.component.html index 087009b291..20bf0475ee 100644 --- a/apps/web/src/app/billing/organizations/organization-billing-history-view.component.html +++ b/apps/web/src/app/billing/organizations/organization-billing-history-view.component.html @@ -1,17 +1,4 @@ - - - + @@ -22,7 +9,16 @@ > {{ "loading" | i18n }} - - + + + diff --git a/apps/web/src/app/billing/organizations/organization-billing-history-view.component.ts b/apps/web/src/app/billing/organizations/organization-billing-history-view.component.ts index cd29345200..00ab3fa777 100644 --- a/apps/web/src/app/billing/organizations/organization-billing-history-view.component.ts +++ b/apps/web/src/app/billing/organizations/organization-billing-history-view.component.ts @@ -2,8 +2,11 @@ import { Component, OnDestroy, OnInit } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; import { concatMap, Subject, takeUntil } from "rxjs"; -import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; -import { BillingHistoryResponse } from "@bitwarden/common/billing/models/response/billing-history.response"; +import { OrganizationBillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/organizations/organization-billing-api.service.abstraction"; +import { + BillingInvoiceResponse, + BillingTransactionResponse, +} from "@bitwarden/common/billing/models/response/billing.response"; @Component({ templateUrl: "organization-billing-history-view.component.html", @@ -11,13 +14,15 @@ import { BillingHistoryResponse } from "@bitwarden/common/billing/models/respons export class OrgBillingHistoryViewComponent implements OnInit, OnDestroy { loading = false; firstLoaded = false; - billing: BillingHistoryResponse; + invoices: BillingInvoiceResponse[] = []; + transactions: BillingTransactionResponse[] = []; organizationId: string; + hasAdditionalHistory: boolean = false; private destroy$ = new Subject(); constructor( - private organizationApiService: OrganizationApiServiceAbstraction, + private organizationBillingApiService: OrganizationBillingApiServiceAbstraction, private route: ActivatedRoute, ) {} @@ -43,8 +48,28 @@ export class OrgBillingHistoryViewComponent implements OnInit, OnDestroy { if (this.loading) { return; } + this.loading = true; - this.billing = await this.organizationApiService.getBillingHistory(this.organizationId); + + const invoicesPromise = this.organizationBillingApiService.getBillingInvoices( + this.organizationId, + this.invoices.length > 0 ? this.invoices[this.invoices.length - 1].id : null, + ); + + const transactionsPromise = this.organizationBillingApiService.getBillingTransactions( + this.organizationId, + this.transactions.length > 0 + ? this.transactions[this.transactions.length - 1].createdDate + : null, + ); + + const invoices = await invoicesPromise; + const transactions = await transactionsPromise; + const pageSize = 5; + + this.invoices = [...this.invoices, ...invoices]; + this.transactions = [...this.transactions, ...transactions]; + this.hasAdditionalHistory = !(invoices.length < pageSize && transactions.length < pageSize); this.loading = false; } } diff --git a/apps/web/src/app/billing/shared/billing-history.component.ts b/apps/web/src/app/billing/shared/billing-history.component.ts index ec85da7d33..ac16b3dc72 100644 --- a/apps/web/src/app/billing/shared/billing-history.component.ts +++ b/apps/web/src/app/billing/shared/billing-history.component.ts @@ -1,7 +1,10 @@ import { Component, Input } from "@angular/core"; import { PaymentMethodType, TransactionType } from "@bitwarden/common/billing/enums"; -import { BillingHistoryResponse } from "@bitwarden/common/billing/models/response/billing-history.response"; +import { + BillingInvoiceResponse, + BillingTransactionResponse, +} from "@bitwarden/common/billing/models/response/billing.response"; @Component({ selector: "app-billing-history", @@ -9,19 +12,14 @@ import { BillingHistoryResponse } from "@bitwarden/common/billing/models/respons }) export class BillingHistoryComponent { @Input() - billing: BillingHistoryResponse; + invoices: BillingInvoiceResponse[]; + + @Input() + transactions: BillingTransactionResponse[]; paymentMethodType = PaymentMethodType; transactionType = TransactionType; - get invoices() { - return this.billing != null ? this.billing.invoices : null; - } - - get transactions() { - return this.billing != null ? this.billing.transactions : null; - } - paymentMethodClasses(type: PaymentMethodType) { switch (type) { case PaymentMethodType.Card: diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 5ccba9ead3..cdf6a27390 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -125,9 +125,13 @@ import { BillingApiServiceAbstraction, OrganizationBillingServiceAbstraction, } from "@bitwarden/common/billing/abstractions"; +import { AccountBillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/account/account-billing-api.service.abstraction"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; +import { OrganizationBillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/organizations/organization-billing-api.service.abstraction"; +import { AccountBillingApiService } from "@bitwarden/common/billing/services/account/account-billing-api.service"; import { DefaultBillingAccountProfileStateService } from "@bitwarden/common/billing/services/account/billing-account-profile-state.service"; import { BillingApiService } from "@bitwarden/common/billing/services/billing-api.service"; +import { OrganizationBillingApiService } from "@bitwarden/common/billing/services/organization/organization-billing-api.service"; import { OrganizationBillingService } from "@bitwarden/common/billing/services/organization-billing.service"; import { AppIdService as AppIdServiceAbstraction } from "@bitwarden/common/platform/abstractions/app-id.service"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; @@ -985,6 +989,16 @@ const safeProviders: SafeProvider[] = [ // subscribes to sync notifications and will update itself based on that. deps: [ApiServiceAbstraction, SyncService], }), + safeProvider({ + provide: OrganizationBillingApiServiceAbstraction, + useClass: OrganizationBillingApiService, + deps: [ApiServiceAbstraction], + }), + safeProvider({ + provide: AccountBillingApiServiceAbstraction, + useClass: AccountBillingApiService, + deps: [ApiServiceAbstraction], + }), safeProvider({ provide: DefaultConfigService, useClass: DefaultConfigService, 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 new file mode 100644 index 0000000000..4b67ce55c3 --- /dev/null +++ b/libs/common/src/billing/abstractions/account/account-billing-api.service.abstraction.ts @@ -0,0 +1,12 @@ +import { + BillingInvoiceResponse, + BillingTransactionResponse, +} from "@bitwarden/common/billing/models/response/billing.response"; + +export class AccountBillingApiServiceAbstraction { + getBillingInvoices: (id: string, startAfter?: string) => Promise; + getBillingTransactions: ( + id: string, + startAfter?: string, + ) => Promise; +} 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 new file mode 100644 index 0000000000..4b3592bb6d --- /dev/null +++ b/libs/common/src/billing/abstractions/organizations/organization-billing-api.service.abstraction.ts @@ -0,0 +1,12 @@ +import { + BillingInvoiceResponse, + BillingTransactionResponse, +} from "@bitwarden/common/billing/models/response/billing.response"; + +export class OrganizationBillingApiServiceAbstraction { + getBillingInvoices: (id: string, startAfter?: string) => Promise; + getBillingTransactions: ( + id: string, + startAfter?: string, + ) => Promise; +} diff --git a/libs/common/src/billing/models/response/billing.response.ts b/libs/common/src/billing/models/response/billing.response.ts index 45d3cf1e67..b94fc1b64b 100644 --- a/libs/common/src/billing/models/response/billing.response.ts +++ b/libs/common/src/billing/models/response/billing.response.ts @@ -29,6 +29,7 @@ export class BillingSourceResponse extends BaseResponse { } export class BillingInvoiceResponse extends BaseResponse { + id: string; url: string; pdfUrl: string; number: string; @@ -38,6 +39,7 @@ export class BillingInvoiceResponse extends BaseResponse { constructor(response: any) { super(response); + this.id = this.getResponseProperty("Id"); this.url = this.getResponseProperty("Url"); this.pdfUrl = this.getResponseProperty("PdfUrl"); this.number = this.getResponseProperty("Number"); diff --git a/libs/common/src/billing/services/account/account-billing-api.service.ts b/libs/common/src/billing/services/account/account-billing-api.service.ts new file mode 100644 index 0000000000..ddd5bad02e --- /dev/null +++ b/libs/common/src/billing/services/account/account-billing-api.service.ts @@ -0,0 +1,34 @@ +import { ApiService } from "../../../abstractions/api.service"; +import { AccountBillingApiServiceAbstraction } from "../../abstractions/account/account-billing-api.service.abstraction"; +import { + BillingInvoiceResponse, + BillingTransactionResponse, +} from "../../models/response/billing.response"; + +export class AccountBillingApiService implements AccountBillingApiServiceAbstraction { + constructor(private apiService: ApiService) {} + + async getBillingInvoices(startAfter?: string): Promise { + const queryParams = startAfter ? `?startAfter=${startAfter}` : ""; + const r = await this.apiService.send( + "GET", + `/accounts/billing/invoices${queryParams}`, + null, + true, + true, + ); + return r?.map((i: any) => new BillingInvoiceResponse(i)) || []; + } + + async getBillingTransactions(startAfter?: string): Promise { + const queryParams = startAfter ? `?startAfter=${startAfter}` : ""; + const r = await this.apiService.send( + "GET", + `/accounts/billing/transactions${queryParams}`, + null, + true, + true, + ); + return r?.map((i: any) => new BillingTransactionResponse(i)) || []; + } +} 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 new file mode 100644 index 0000000000..acf12e8320 --- /dev/null +++ b/libs/common/src/billing/services/organization/organization-billing-api.service.ts @@ -0,0 +1,37 @@ +import { ApiService } from "../../../abstractions/api.service"; +import { OrganizationBillingApiServiceAbstraction } from "../../abstractions/organizations/organization-billing-api.service.abstraction"; +import { + BillingInvoiceResponse, + BillingTransactionResponse, +} from "../../models/response/billing.response"; + +export class OrganizationBillingApiService implements OrganizationBillingApiServiceAbstraction { + constructor(private apiService: ApiService) {} + + async getBillingInvoices(id: string, startAfter?: string): Promise { + const queryParams = startAfter ? `?startAfter=${startAfter}` : ""; + const r = await this.apiService.send( + "GET", + `/organizations/${id}/billing/invoices${queryParams}`, + null, + true, + true, + ); + return r?.map((i: any) => new BillingInvoiceResponse(i)) || []; + } + + async getBillingTransactions( + id: string, + startAfter?: string, + ): Promise { + const queryParams = startAfter ? `?startAfter=${startAfter}` : ""; + const r = await this.apiService.send( + "GET", + `/organizations/${id}/billing/transactions${queryParams}`, + null, + true, + true, + ); + return r?.map((i: any) => new BillingTransactionResponse(i)) || []; + } +}