diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/providers.module.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/providers.module.ts index 2d4b39fa8b..baa3e5e1bb 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/providers.module.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/providers.module.ts @@ -18,6 +18,7 @@ import { ProviderSelectPaymentMethodDialogComponent, ProviderSubscriptionComponent, } from "../../billing/providers"; +import { SubscriptionStatusComponent } from "../../billing/providers/subscription/subscription-status.component"; import { AddOrganizationComponent } from "./clients/add-organization.component"; import { ClientsComponent } from "./clients/clients.component"; @@ -70,6 +71,7 @@ import { SetupComponent } from "./setup/setup.component"; ProviderSubscriptionComponent, ProviderSelectPaymentMethodDialogComponent, ProviderPaymentMethodComponent, + SubscriptionStatusComponent, ], providers: [WebProviderService, ProviderPermissionsGuard], }) 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 fdcb8a6701..47f8aa375c 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 @@ -1,32 +1,10 @@ - {{ "loading" | i18n }} - - - - {{ "subscriptionCanceled" | i18n }} - -
-
{{ "billingPlan" | i18n }}
-
{{ "providerPlan" | i18n }}
- -
{{ "status" | i18n }}
-
- {{ subscription.status }} -
-
{{ "nextCharge" | i18n }}
-
- {{ subscription.currentPeriodEndDate | date: "mediumDate" }} -
-
-
-
- +
+ +

{{ data.callout.body }}

+ +
+
+
{{ "billingPlan" | i18n }}
+
{{ "providerPlan" | i18n }}
+ +
{{ data.status.label }}
+
+ + {{ displayedStatus }} + +
+
+ {{ data.date.label | titlecase }} +
+
+ {{ data.date.value | date: "mediumDate" }} +
+
+
+ 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/subscription-status.component.ts new file mode 100644 index 0000000000..fa9a892254 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/billing/providers/subscription/subscription-status.component.ts @@ -0,0 +1,188 @@ +import { DatePipe } from "@angular/common"; +import { Component, EventEmitter, Input, Output } from "@angular/core"; + +import { ProviderSubscriptionResponse } from "@bitwarden/common/billing/models/response/provider-subscription-response"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; + +type ComponentData = { + status?: { + label: string; + value: string; + }; + date?: { + label: string; + value: string; + }; + callout?: { + severity: "danger" | "warning"; + header: string; + body: string; + showReinstatementButton: boolean; + }; +}; + +@Component({ + selector: "app-subscription-status", + templateUrl: "subscription-status.component.html", +}) +export class SubscriptionStatusComponent { + @Input({ required: true }) providerSubscriptionResponse: ProviderSubscriptionResponse; + @Output() reinstatementRequested = new EventEmitter(); + + 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 { + 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"); + + const nextChargeDateLabel = this.i18nService.t("nextCharge"); + const subscriptionExpiredDateLabel = this.i18nService.t("subscriptionExpired"); + 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: { + label: defaultStatusLabel, + value: this.i18nService.t("active"), + }, + date: { + label: nextChargeDateLabel, + value: this.subscription.currentPeriodEndDate.toDateString(), + }, + }; + } + case "past_due": { + const pastDueText = this.i18nService.t("pastDue"); + const suspensionDate = this.datePipe.transform( + this.subscription.suspensionDate, + "mediumDate", + ); + const calloutBody = + this.subscription.collectionMethod === "charge_automatically" + ? this.i18nService.t( + "pastDueWarningForChargeAutomatically", + this.subscription.gracePeriod, + suspensionDate, + ) + : this.i18nService.t( + "pastDueWarningForSendInvoice", + this.subscription.gracePeriod, + suspensionDate, + ); + return { + status: { + label: defaultStatusLabel, + value: pastDueText, + }, + date: { + label: subscriptionExpiredDateLabel, + value: this.subscription.unpaidPeriodEndDate, + }, + callout: { + severity: "warning", + header: pastDueText, + body: calloutBody, + showReinstatementButton: false, + }, + }; + } + case "unpaid": { + return { + status: { + label: defaultStatusLabel, + value: this.i18nService.t("unpaid"), + }, + date: { + label: subscriptionExpiredDateLabel, + value: this.subscription.currentPeriodEndDate.toDateString(), + }, + callout: { + severity: "danger", + header: this.i18nService.t("unpaidInvoice"), + body: this.i18nService.t("toReactivateYourSubscription"), + showReinstatementButton: false, + }, + }; + } + case "pending_cancellation": { + const pendingCancellationText = this.i18nService.t("pendingCancellation"); + return { + status: { + label: defaultStatusLabel, + value: pendingCancellationText, + }, + date: { + label: cancellationDateLabel, + value: this.subscription.currentPeriodEndDate.toDateString(), + }, + callout: { + severity: "warning", + header: pendingCancellationText, + body: this.i18nService.t("subscriptionPendingCanceled"), + showReinstatementButton: true, + }, + }; + } + case "incomplete_expired": + case "canceled": { + const canceledText = this.i18nService.t("canceled"); + return { + status: { + label: defaultStatusLabel, + value: canceledText, + }, + date: { + label: cancellationDateLabel, + value: this.subscription.currentPeriodEndDate.toDateString(), + }, + callout: { + severity: "danger", + header: canceledText, + body: this.i18nService.t("subscriptionCanceled"), + showReinstatementButton: false, + }, + }; + } + } + } + + requestReinstatement = () => this.reinstatementRequested.emit(); +} 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 522c518725..9f6823f71f 100644 --- a/libs/common/src/billing/models/response/provider-subscription-response.ts +++ b/libs/common/src/billing/models/response/provider-subscription-response.ts @@ -4,6 +4,10 @@ export class ProviderSubscriptionResponse extends BaseResponse { status: string; currentPeriodEndDate: Date; discountPercentage?: number | null; + collectionMethod: string; + unpaidPeriodEndDate?: string; + gracePeriod?: number | null; + suspensionDate?: string; plans: Plans[] = []; constructor(response: any) { @@ -11,6 +15,10 @@ export class ProviderSubscriptionResponse extends BaseResponse { this.status = this.getResponseProperty("status"); this.currentPeriodEndDate = new Date(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"); const plans = this.getResponseProperty("plans"); if (plans != null) { this.plans = plans.map((i: any) => new Plans(i));