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));