From 679c25b082db9e01b6f5d37b6adf4fc379acac81 Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Wed, 26 Jun 2024 09:08:25 -0400 Subject: [PATCH] Combined subscription and payment method pages in provider portal (#9828) --- .../providers/providers-layout.component.html | 1 - .../providers/providers-routing.module.ts | 9 -- .../providers/providers.module.ts | 4 +- ...ovider-subscription-status.component.html} | 11 +- ...provider-subscription-status.component.ts} | 65 ++------- .../provider-subscription.component.html | 131 +++++++++++------- .../provider-subscription.component.ts | 33 ++++- .../provider-subscription-response.ts | 31 +++-- .../subscription-suspension.response.ts | 15 ++ 9 files changed, 160 insertions(+), 140 deletions(-) rename bitwarden_license/bit-web/src/app/billing/providers/subscription/{subscription-status.component.html => provider-subscription-status.component.html} (71%) rename bitwarden_license/bit-web/src/app/billing/providers/subscription/{subscription-status.component.ts => provider-subscription-status.component.ts} (68%) create mode 100644 libs/common/src/billing/models/response/subscription-suspension.response.ts diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.html b/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.html index 3bd5053994..5a46b49f24 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.html +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.html @@ -31,7 +31,6 @@ *ngIf="canAccessBilling$ | async" > - {{ data.callout.body }} - - {{ "reinstateSubscription" | i18n }} - {{ "billingPlan" | i18n }} @@ -18,7 +9,7 @@ {{ data.status.label }} - {{ displayedStatus }} + {{ data.status.value }} diff --git a/bitwarden_license/bit-web/src/app/billing/providers/subscription/subscription-status.component.ts b/bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription-status.component.ts similarity index 68% rename from bitwarden_license/bit-web/src/app/billing/providers/subscription/subscription-status.component.ts rename to bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription-status.component.ts index 91cdef10ac..c3ad875136 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/subscription/subscription-status.component.ts +++ b/bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription-status.component.ts @@ -1,5 +1,5 @@ import { DatePipe } from "@angular/common"; -import { Component, EventEmitter, Input, Output } from "@angular/core"; +import { Component, Input } from "@angular/core"; import { ProviderSubscriptionResponse } from "@bitwarden/common/billing/models/response/provider-subscription-response"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -17,47 +17,29 @@ type ComponentData = { severity: "danger" | "warning"; header: string; body: string; - showReinstatementButton: boolean; }; }; @Component({ - selector: "app-subscription-status", - templateUrl: "subscription-status.component.html", + selector: "app-provider-subscription-status", + templateUrl: "provider-subscription-status.component.html", }) -export class SubscriptionStatusComponent { - @Input({ required: true }) providerSubscriptionResponse: ProviderSubscriptionResponse; - @Output() reinstatementRequested = new EventEmitter(); +export class ProviderSubscriptionStatusComponent { + @Input({ required: true }) subscription: ProviderSubscriptionResponse; constructor( private datePipe: DatePipe, private i18nService: I18nService, ) {} - get displayedStatus(): string { - return this.data.status.value; - } - - get planName() { - return this.providerSubscriptionResponse.plans[0]; - } - get status(): string { if (this.subscription.cancelAt && this.subscription.status === "active") { - this.subscription.status = "pending_cancellation"; + return "pending_cancellation"; } return this.subscription.status; } - get isExpired() { - return this.subscription.status !== "active"; - } - - get subscription() { - return this.providerSubscriptionResponse; - } - get data(): ComponentData { const defaultStatusLabel = this.i18nService.t("status"); @@ -66,21 +48,6 @@ export class SubscriptionStatusComponent { const cancellationDateLabel = this.i18nService.t("cancellationDate"); switch (this.status) { - case "free": { - return {}; - } - case "trialing": { - return { - status: { - label: defaultStatusLabel, - value: this.i18nService.t("trial"), - }, - date: { - label: nextChargeDateLabel, - value: this.subscription.currentPeriodEndDate.toDateString(), - }, - }; - } case "active": { return { status: { @@ -89,26 +56,26 @@ export class SubscriptionStatusComponent { }, date: { label: nextChargeDateLabel, - value: this.subscription.currentPeriodEndDate.toDateString(), + value: this.subscription.currentPeriodEndDate, }, }; } case "past_due": { const pastDueText = this.i18nService.t("pastDue"); const suspensionDate = this.datePipe.transform( - this.subscription.suspensionDate, + this.subscription.suspension.suspensionDate, "mediumDate", ); const calloutBody = this.subscription.collectionMethod === "charge_automatically" ? this.i18nService.t( "pastDueWarningForChargeAutomatically", - this.subscription.gracePeriod, + this.subscription.suspension.gracePeriod, suspensionDate, ) : this.i18nService.t( "pastDueWarningForSendInvoice", - this.subscription.gracePeriod, + this.subscription.suspension.gracePeriod, suspensionDate, ); return { @@ -118,13 +85,12 @@ export class SubscriptionStatusComponent { }, date: { label: subscriptionExpiredDateLabel, - value: this.subscription.unpaidPeriodEndDate, + value: this.subscription.suspension.unpaidPeriodEndDate, }, callout: { severity: "warning", header: pastDueText, body: calloutBody, - showReinstatementButton: false, }, }; } @@ -136,13 +102,12 @@ export class SubscriptionStatusComponent { }, date: { label: subscriptionExpiredDateLabel, - value: this.subscription.currentPeriodEndDate.toDateString(), + value: this.subscription.suspension.unpaidPeriodEndDate, }, callout: { severity: "danger", header: this.i18nService.t("unpaidInvoice"), body: this.i18nService.t("toReactivateYourSubscription"), - showReinstatementButton: false, }, }; } @@ -163,7 +128,6 @@ export class SubscriptionStatusComponent { body: this.i18nService.t("subscriptionPendingCanceled") + this.i18nService.t("providerReinstate"), - showReinstatementButton: false, }, }; } @@ -177,18 +141,15 @@ export class SubscriptionStatusComponent { }, date: { label: cancellationDateLabel, - value: this.subscription.currentPeriodEndDate.toDateString(), + value: this.subscription.currentPeriodEndDate, }, callout: { severity: "danger", header: canceledText, body: this.i18nService.t("subscriptionCanceled"), - showReinstatementButton: false, }, }; } } } - - requestReinstatement = () => this.reinstatementRequested.emit(); } diff --git a/bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription.component.html b/bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription.component.html index 47f8aa375c..d447495387 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription.component.html +++ b/bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription.component.html @@ -4,58 +4,85 @@ {{ "loading" | i18n }} - - - - {{ "details" | i18n }} {{ "providerDiscount" | i18n: subscription.discountPercentage }} - - - - - - - {{ getFormattedPlanName(i.planName) }} {{ "orgSeats" | i18n }} ({{ - i.cadence.toLowerCase() - }}) {{ "×" }}{{ getFormattedSeatCount(i.seatMinimum, i.purchasedSeats) }} - @ - {{ - getFormattedCost( - i.cost, - i.seatMinimum, - i.purchasedSeats, - subscription.discountPercentage - ) | currency: "$" - }} - - - {{ ((100 - subscription.discountPercentage) / 100) * i.cost | currency: "$" }} /{{ - "month" | i18n - }} - - - {{ i.cost | currency: "$" }} /{{ "month" | i18n }} - - - - + + + + + {{ "details" | i18n }} {{ "providerDiscount" | i18n: subscription.discountPercentage }} + + + + + + + {{ getFormattedPlanName(i.planName) }} {{ "orgSeats" | i18n }} ({{ + i.cadence.toLowerCase() + }}) {{ "×" }}{{ getFormattedSeatCount(i.seatMinimum, i.purchasedSeats) }} + @ + {{ + getFormattedCost( + i.cost, + i.seatMinimum, + i.purchasedSeats, + subscription.discountPercentage + ) | currency: "$" + }} + + + {{ ((100 - subscription.discountPercentage) / 100) * i.cost | currency: "$" }} /{{ + "month" | i18n + }} + + + {{ i.cost | currency: "$" }} /{{ "month" | i18n }} + + + + - - - - Total: {{ totalCost | currency: "$" }} /{{ - "month" | i18n - }} - - - - - - + + + + Total: {{ totalCost | currency: "$" }} /{{ + "month" | i18n + }} + + + + + + + + + + + {{ "accountCredit" | i18n }} + + {{ subscription.accountCredit | currency: "$" }} + {{ "creditAppliedDesc" | i18n }} + + {{ "addCredit" | i18n }} + + + + + {{ "taxInformation" | i18n }} + {{ "taxInformationDesc" | i18n }} + + 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 ca405747bf..d582ad071f 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 @@ -2,19 +2,25 @@ import { Component } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; import { Subject, concatMap, takeUntil } from "rxjs"; +import { openAddAccountCreditDialog } from "@bitwarden/angular/billing/components"; import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billilng-api.service.abstraction"; +import { TaxInformation } from "@bitwarden/common/billing/models/domain"; +import { ExpandedTaxInfoUpdateRequest } from "@bitwarden/common/billing/models/request/expanded-tax-info-update.request"; import { ProviderPlanResponse, ProviderSubscriptionResponse, } from "@bitwarden/common/billing/models/response/provider-subscription-response"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { DialogService, ToastService } from "@bitwarden/components"; @Component({ selector: "app-provider-subscription", templateUrl: "./provider-subscription.component.html", }) export class ProviderSubscriptionComponent { - subscription: ProviderSubscriptionResponse; providerId: string; + subscription: ProviderSubscriptionResponse; + firstLoaded = false; loading: boolean; private destroy$ = new Subject(); @@ -23,7 +29,10 @@ export class ProviderSubscriptionComponent { constructor( private billingApiService: BillingApiServiceAbstraction, + private dialogService: DialogService, + private i18nService: I18nService, private route: ActivatedRoute, + private toastService: ToastService, ) {} async ngOnInit() { @@ -54,6 +63,23 @@ export class ProviderSubscriptionComponent { this.loading = false; } + addAccountCredit = () => + openAddAccountCreditDialog(this.dialogService, { + data: { + providerId: this.providerId, + }, + }); + + 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"), + }); + }; + getFormattedCost( cost: number, seatMinimum: number, @@ -61,8 +87,7 @@ export class ProviderSubscriptionComponent { discountPercentage: number, ): number { const costPerSeat = cost / (seatMinimum + purchasedSeats); - const discountedCost = costPerSeat - (costPerSeat * discountPercentage) / 100; - return discountedCost; + return costPerSeat - (costPerSeat * discountPercentage) / 100; } getFormattedPlanName(planName: string): string { @@ -83,4 +108,6 @@ export class ProviderSubscriptionComponent { this.destroy$.next(); this.destroy$.complete(); } + + protected readonly TaxInformation = TaxInformation; } diff --git a/libs/common/src/billing/models/response/provider-subscription-response.ts b/libs/common/src/billing/models/response/provider-subscription-response.ts index 4986914cc0..2dc9d4281d 100644 --- a/libs/common/src/billing/models/response/provider-subscription-response.ts +++ b/libs/common/src/billing/models/response/provider-subscription-response.ts @@ -1,29 +1,38 @@ +import { SubscriptionSuspensionResponse } from "@bitwarden/common/billing/models/response/subscription-suspension.response"; +import { TaxInfoResponse } from "@bitwarden/common/billing/models/response/tax-info.response"; + import { BaseResponse } from "../../../models/response/base.response"; export class ProviderSubscriptionResponse extends BaseResponse { status: string; - currentPeriodEndDate: Date; + currentPeriodEndDate: string; discountPercentage?: number | null; - plans: ProviderPlanResponse[] = []; collectionMethod: string; - unpaidPeriodEndDate?: string; - gracePeriod?: number | null; - suspensionDate?: string; + plans: ProviderPlanResponse[] = []; + accountCredit: number; + taxInformation?: TaxInfoResponse; cancelAt?: string; + suspension?: SubscriptionSuspensionResponse; constructor(response: any) { super(response); this.status = this.getResponseProperty("status"); - this.currentPeriodEndDate = new Date(this.getResponseProperty("currentPeriodEndDate")); + this.currentPeriodEndDate = this.getResponseProperty("currentPeriodEndDate"); this.discountPercentage = this.getResponseProperty("discountPercentage"); this.collectionMethod = this.getResponseProperty("collectionMethod"); - this.unpaidPeriodEndDate = this.getResponseProperty("unpaidPeriodEndDate"); - this.gracePeriod = this.getResponseProperty("gracePeriod"); - this.suspensionDate = this.getResponseProperty("suspensionDate"); - this.cancelAt = this.getResponseProperty("cancelAt"); const plans = this.getResponseProperty("plans"); if (plans != null) { - this.plans = plans.map((i: any) => new ProviderPlanResponse(i)); + this.plans = plans.map((plan: any) => new ProviderPlanResponse(plan)); + } + this.accountCredit = this.getResponseProperty("accountCredit"); + const taxInformation = this.getResponseProperty("taxInformation"); + if (taxInformation != null) { + this.taxInformation = new TaxInfoResponse(taxInformation); + } + this.cancelAt = this.getResponseProperty("cancelAt"); + const suspension = this.getResponseProperty("suspension"); + if (suspension != null) { + this.suspension = new SubscriptionSuspensionResponse(suspension); } } } diff --git a/libs/common/src/billing/models/response/subscription-suspension.response.ts b/libs/common/src/billing/models/response/subscription-suspension.response.ts new file mode 100644 index 0000000000..418e1c443c --- /dev/null +++ b/libs/common/src/billing/models/response/subscription-suspension.response.ts @@ -0,0 +1,15 @@ +import { BaseResponse } from "@bitwarden/common/models/response/base.response"; + +export class SubscriptionSuspensionResponse extends BaseResponse { + suspensionDate: string; + unpaidPeriodEndDate: string; + gracePeriod: number; + + constructor(response: any) { + super(response); + + this.suspensionDate = this.getResponseProperty("suspensionDate"); + this.unpaidPeriodEndDate = this.getResponseProperty("unpaidPeriodEndDate"); + this.gracePeriod = this.getResponseProperty("gracePeriod"); + } +}
{{ data.callout.body }}
{{ subscription.accountCredit | currency: "$" }}
{{ "creditAppliedDesc" | i18n }}
{{ "taxInformationDesc" | i18n }}