1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-11-25 12:15:18 +01:00

[AC-2721][Defect] Apply Subscription Status Updates in Provider Subscription details (#9484)

* Add status banner to the provider subscription page

* Add the isexpired method

* Add the unpaid status banner
This commit is contained in:
cyprain-okeke 2024-06-06 15:53:52 +01:00 committed by GitHub
parent a7b45a44e3
commit 0f9f7f4df6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 231 additions and 23 deletions

View File

@ -18,6 +18,7 @@ import {
ProviderSelectPaymentMethodDialogComponent, ProviderSelectPaymentMethodDialogComponent,
ProviderSubscriptionComponent, ProviderSubscriptionComponent,
} from "../../billing/providers"; } from "../../billing/providers";
import { SubscriptionStatusComponent } from "../../billing/providers/subscription/subscription-status.component";
import { AddOrganizationComponent } from "./clients/add-organization.component"; import { AddOrganizationComponent } from "./clients/add-organization.component";
import { ClientsComponent } from "./clients/clients.component"; import { ClientsComponent } from "./clients/clients.component";
@ -70,6 +71,7 @@ import { SetupComponent } from "./setup/setup.component";
ProviderSubscriptionComponent, ProviderSubscriptionComponent,
ProviderSelectPaymentMethodDialogComponent, ProviderSelectPaymentMethodDialogComponent,
ProviderPaymentMethodComponent, ProviderPaymentMethodComponent,
SubscriptionStatusComponent,
], ],
providers: [WebProviderService, ProviderPermissionsGuard], providers: [WebProviderService, ProviderPermissionsGuard],
}) })

View File

@ -1,32 +1,10 @@
<app-header></app-header> <app-header></app-header>
<bit-container> <bit-container>
<ng-container *ngIf="!firstLoaded && loading"> <ng-container *ngIf="!firstLoaded && loading">
<i class="bwi bwi-spinner bwi-spin text-muted" title="{{ 'loading' | i18n }}"></i> <i class="bwi bwi-spinner bwi-spin text-muted" title="{{ 'loading' | i18n }}"></i>
<span class="sr-only">{{ "loading" | i18n }}</span> <span class="sr-only">{{ "loading" | i18n }}</span>
</ng-container> </ng-container>
<app-subscription-status [providerSubscriptionResponse]="subscription"> </app-subscription-status>
<ng-container *ngIf="subscription && firstLoaded">
<bit-callout type="warning" title="{{ 'canceled' | i18n }}" *ngIf="false">
{{ "subscriptionCanceled" | i18n }}</bit-callout
>
<dl class="tw-grid tw-grid-flow-col tw-grid-rows-2">
<dt>{{ "billingPlan" | i18n }}</dt>
<dd>{{ "providerPlan" | i18n }}</dd>
<ng-container *ngIf="subscription">
<dt>{{ "status" | i18n }}</dt>
<dd>
<span class="tw-capitalize">{{ subscription.status }}</span>
</dd>
<dt [ngClass]="{ 'tw-text-danger': isExpired }">{{ "nextCharge" | i18n }}</dt>
<dd [ngClass]="{ 'tw-text-danger': isExpired }">
{{ subscription.currentPeriodEndDate | date: "mediumDate" }}
</dd>
</ng-container>
</dl>
</ng-container>
<ng-container> <ng-container>
<div class="tw-flex-col"> <div class="tw-flex-col">
<strong class="tw-block tw-border-0 tw-border-b tw-border-solid tw-border-secondary-300 pb-2" <strong class="tw-block tw-border-0 tw-border-b tw-border-solid tw-border-secondary-300 pb-2"

View File

@ -0,0 +1,32 @@
<ng-container>
<bit-callout *ngIf="data.callout" [type]="data.callout.severity" [title]="data.callout.header">
<p>{{ data.callout.body }}</p>
<button
*ngIf="data.callout.showReinstatementButton"
bitButton
buttonType="secondary"
[bitAction]="requestReinstatement"
type="button"
>
{{ "reinstateSubscription" | i18n }}
</button>
</bit-callout>
<dl class="tw-grid tw-grid-flow-col tw-grid-rows-2">
<dt>{{ "billingPlan" | i18n }}</dt>
<dd>{{ "providerPlan" | i18n }}</dd>
<ng-container *ngIf="data.status && data.date">
<dt>{{ data.status.label }}</dt>
<dd>
<span class="tw-capitalize">
{{ displayedStatus }}
</span>
</dd>
<dt [ngClass]="{ 'tw-text-danger': isExpired }">
{{ data.date.label | titlecase }}
</dt>
<dd [ngClass]="{ 'tw-text-danger': isExpired }">
{{ data.date.value | date: "mediumDate" }}
</dd>
</ng-container>
</dl>
</ng-container>

View File

@ -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<void>();
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();
}

View File

@ -4,6 +4,10 @@ export class ProviderSubscriptionResponse extends BaseResponse {
status: string; status: string;
currentPeriodEndDate: Date; currentPeriodEndDate: Date;
discountPercentage?: number | null; discountPercentage?: number | null;
collectionMethod: string;
unpaidPeriodEndDate?: string;
gracePeriod?: number | null;
suspensionDate?: string;
plans: Plans[] = []; plans: Plans[] = [];
constructor(response: any) { constructor(response: any) {
@ -11,6 +15,10 @@ export class ProviderSubscriptionResponse extends BaseResponse {
this.status = this.getResponseProperty("status"); this.status = this.getResponseProperty("status");
this.currentPeriodEndDate = new Date(this.getResponseProperty("currentPeriodEndDate")); this.currentPeriodEndDate = new Date(this.getResponseProperty("currentPeriodEndDate"));
this.discountPercentage = this.getResponseProperty("discountPercentage"); 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"); const plans = this.getResponseProperty("plans");
if (plans != null) { if (plans != null) {
this.plans = plans.map((i: any) => new Plans(i)); this.plans = plans.map((i: any) => new Plans(i));