[AC-358] SelfHosted update subscription page (#5101)
* [AC-358] Add selfHostSubscriptionExpiration property to organization-subscription.response.ts * [AC-358] Update selfHost org subscription template - Replace "Subscription" with "SubscriptionExpiration" - Add question mark help link - Add helper text for grace period - Add support for graceful fallback in case of missing grace period in subscription response * Update libs/common/src/billing/models/response/organization-subscription.response.ts Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com> * [AC-358] Remove unnecessary hypen Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com> * [AC-358] Introduce SelfHostedOrganizationSubscription view - Encapsulate expiration/grace period logic in the new view object. - Remove API response getters from the angular component - Replace the API response object with the new view * [AC-358] Clarify name for new expiration without grace period field * [AC-358] Update constructor parameter name * [AC-358] Simplify new selfhost subscription view - Make expiration date properties public - Remove obsolete expiration date getters - Update the component to use new properties - Add helper to component for determining if the subscription should be rendered as expired (red text) * [AC-358] Rename isExpired to isExpiredAndOutsideGracePeriod to be more explicit --------- Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com>
This commit is contained in:
parent
44fd063dc1
commit
bcda04ee86
|
@ -22,25 +22,45 @@
|
|||
[providerName]="userOrg.providerName"
|
||||
></app-org-subscription-hidden>
|
||||
|
||||
<ng-container *ngIf="sub && firstLoaded">
|
||||
<ng-container *ngIf="subscription && firstLoaded">
|
||||
<dl>
|
||||
<dt>{{ "billingPlan" | i18n }}</dt>
|
||||
<dd>{{ sub.plan.name }}</dd>
|
||||
<dt>{{ "expiration" | i18n }}</dt>
|
||||
<dd *ngIf="sub.expiration">
|
||||
{{ sub.expiration | date : "mediumDate" }}
|
||||
<span *ngIf="isExpired" class="text-danger ml-2">
|
||||
<i class="bwi bwi-exclamation-triangle" aria-hidden="true"></i>
|
||||
{{ "licenseIsExpired" | i18n }}
|
||||
</span>
|
||||
</dd>
|
||||
<dd *ngIf="!sub.expiration">{{ "neverExpires" | i18n }}</dd>
|
||||
<dd>{{ subscription.planName }}</dd>
|
||||
<ng-container *ngIf="billingSyncSetUp">
|
||||
<dt>{{ "lastLicenseSync" | i18n }}</dt>
|
||||
<dd>
|
||||
{{ lastLicenseSync != null ? (lastLicenseSync | date : "medium") : ("never" | i18n) }}
|
||||
</dd>
|
||||
</ng-container>
|
||||
<dt>
|
||||
<span [ngClass]="{ 'tw-text-danger': showAsExpired }">{{
|
||||
"subscriptionExpiration" | i18n
|
||||
}}</span>
|
||||
<a
|
||||
href="https://bitwarden.com/help/licensing-on-premise/#update-a-renewed-organization-license "
|
||||
target="_blank"
|
||||
[appA11yTitle]="'licensePaidFeaturesHelp' | i18n"
|
||||
rel="noopener"
|
||||
>
|
||||
<i class="bwi bwi-question-circle" aria-hidden="true"></i>
|
||||
<span class="sr-only">{{ "licensePaidFeaturesHelp" | i18n }}</span>
|
||||
</a>
|
||||
</dt>
|
||||
<dd *ngIf="subscription.hasExpiration" [ngClass]="{ 'tw-text-danger': showAsExpired }">
|
||||
{{
|
||||
(subscription.hasSeparateGracePeriod
|
||||
? subscription.expirationWithoutGracePeriod
|
||||
: subscription.expirationWithGracePeriod
|
||||
) | date : "mediumDate"
|
||||
}}
|
||||
<div *ngIf="subscription.hasSeparateGracePeriod" class="tw-text-muted">
|
||||
{{
|
||||
"selfHostGracePeriodHelp"
|
||||
| i18n : (subscription.expirationWithGracePeriod | date : "mediumDate")
|
||||
}}
|
||||
</div>
|
||||
</dd>
|
||||
<dd *ngIf="!subscription.hasExpiration">{{ "neverExpires" | i18n }}</dd>
|
||||
</dl>
|
||||
|
||||
<a
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { Component, OnDestroy, OnInit } from "@angular/core";
|
||||
import { FormControl, FormGroup } from "@angular/forms";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
import { concatMap, takeUntil, Subject } from "rxjs";
|
||||
import { concatMap, Subject, takeUntil } from "rxjs";
|
||||
|
||||
import { ModalConfig, ModalService } from "@bitwarden/angular/services/modal.service";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
|
@ -14,7 +14,7 @@ import { OrganizationConnectionType } from "@bitwarden/common/admin-console/enum
|
|||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { OrganizationConnectionResponse } from "@bitwarden/common/admin-console/models/response/organization-connection.response";
|
||||
import { BillingSyncConfigApi } from "@bitwarden/common/billing/models/api/billing-sync-config.api";
|
||||
import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.response";
|
||||
import { SelfHostedOrganizationSubscriptionView } from "@bitwarden/common/billing/models/view/self-hosted-organization-subscription.view";
|
||||
|
||||
import {
|
||||
BillingSyncKeyComponent,
|
||||
|
@ -31,7 +31,7 @@ enum LicenseOptions {
|
|||
templateUrl: "organization-subscription-selfhost.component.html",
|
||||
})
|
||||
export class OrganizationSubscriptionSelfhostComponent implements OnInit, OnDestroy {
|
||||
sub: OrganizationSubscriptionResponse;
|
||||
subscription: SelfHostedOrganizationSubscriptionView;
|
||||
organizationId: string;
|
||||
userOrg: Organization;
|
||||
|
||||
|
@ -65,6 +65,15 @@ export class OrganizationSubscriptionSelfhostComponent implements OnInit, OnDest
|
|||
return this.existingBillingSyncConnection?.enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the subscription as expired.
|
||||
*/
|
||||
get showAsExpired() {
|
||||
return this.subscription.hasSeparateGracePeriod
|
||||
? this.subscription.isExpiredWithoutGracePeriod
|
||||
: this.subscription.isExpiredAndOutsideGracePeriod;
|
||||
}
|
||||
|
||||
constructor(
|
||||
private modalService: ModalService,
|
||||
private messagingService: MessagingService,
|
||||
|
@ -102,7 +111,10 @@ export class OrganizationSubscriptionSelfhostComponent implements OnInit, OnDest
|
|||
this.loading = true;
|
||||
this.userOrg = this.organizationService.get(this.organizationId);
|
||||
if (this.userOrg.canViewSubscription) {
|
||||
this.sub = await this.organizationApiService.getSubscription(this.organizationId);
|
||||
const subscriptionResponse = await this.organizationApiService.getSubscription(
|
||||
this.organizationId
|
||||
);
|
||||
this.subscription = new SelfHostedOrganizationSubscriptionView(subscriptionResponse);
|
||||
}
|
||||
|
||||
this.loading = false;
|
||||
|
@ -159,10 +171,6 @@ export class OrganizationSubscriptionSelfhostComponent implements OnInit, OnDest
|
|||
return this.existingBillingSyncConnection?.id != null;
|
||||
}
|
||||
|
||||
get isExpired() {
|
||||
return this.sub?.expiration != null && new Date(this.sub.expiration) < new Date();
|
||||
}
|
||||
|
||||
get updateMethod() {
|
||||
return this.form.get("updateMethod").value;
|
||||
}
|
||||
|
|
|
@ -6553,6 +6553,18 @@
|
|||
"billingSyncHelp": {
|
||||
"message": "Billing Sync help"
|
||||
},
|
||||
"licensePaidFeaturesHelp": {
|
||||
"message": "License paid features help"
|
||||
},
|
||||
"selfHostGracePeriodHelp": {
|
||||
"message": "After your subscription expires, you have 60 days to apply an updated license file to your organization. Grace period ends $GRACE_PERIOD_END_DATE$.",
|
||||
"placeholders": {
|
||||
"GRACE_PERIOD_END_DATE": {
|
||||
"content": "$1",
|
||||
"example": "May 12, 2024"
|
||||
}
|
||||
}
|
||||
},
|
||||
"uploadLicense": {
|
||||
"message": "Upload license"
|
||||
},
|
||||
|
|
|
@ -11,6 +11,7 @@ export class OrganizationSubscriptionResponse extends OrganizationResponse {
|
|||
subscription: BillingSubscriptionResponse;
|
||||
upcomingInvoice: BillingSubscriptionUpcomingInvoiceResponse;
|
||||
expiration: string;
|
||||
expirationWithoutGracePeriod: string;
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
|
@ -24,5 +25,6 @@ export class OrganizationSubscriptionResponse extends OrganizationResponse {
|
|||
? null
|
||||
: new BillingSubscriptionUpcomingInvoiceResponse(upcomingInvoice);
|
||||
this.expiration = this.getResponseProperty("Expiration");
|
||||
this.expirationWithoutGracePeriod = this.getResponseProperty("ExpirationWithoutGracePeriod");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
import { View } from "../../../models/view/view";
|
||||
import { OrganizationSubscriptionResponse } from "../response/organization-subscription.response";
|
||||
|
||||
export class SelfHostedOrganizationSubscriptionView implements View {
|
||||
planName: string;
|
||||
|
||||
/**
|
||||
* Date the subscription expires, including the grace period.
|
||||
*/
|
||||
expirationWithGracePeriod?: Date;
|
||||
|
||||
/**
|
||||
* Date the subscription expires, excluding the grace period.
|
||||
* This will be `null` for older (< v12) license files because they do not include this date.
|
||||
* In this case, you have to rely on the `expirationWithGracePeriod` instead.
|
||||
*/
|
||||
expirationWithoutGracePeriod?: Date;
|
||||
|
||||
constructor(response: OrganizationSubscriptionResponse) {
|
||||
if (response == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.planName = response.plan.name;
|
||||
this.expirationWithGracePeriod =
|
||||
response.expiration != null ? new Date(response.expiration) : null;
|
||||
this.expirationWithoutGracePeriod =
|
||||
response.expirationWithoutGracePeriod != null
|
||||
? new Date(response.expirationWithoutGracePeriod)
|
||||
: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The subscription has separate expiration dates for the subscription and the end of grace period.
|
||||
*/
|
||||
get hasSeparateGracePeriod() {
|
||||
return this.expirationWithGracePeriod != null && this.expirationWithoutGracePeriod != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* True if the subscription has an expiration date.
|
||||
*/
|
||||
get hasExpiration() {
|
||||
return this.expirationWithGracePeriod != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* True if the subscription has an expiration date that has past, but may still be within the grace period.
|
||||
* For older licenses (< v12), this will always be false because they do not include the `expirationWithoutGracePeriod`.
|
||||
*/
|
||||
get isExpiredWithoutGracePeriod() {
|
||||
return this.hasSeparateGracePeriod && this.expirationWithoutGracePeriod < new Date();
|
||||
}
|
||||
|
||||
/**
|
||||
* True if the subscription has an expiration date that has past, including the grace period.
|
||||
*/
|
||||
get isExpiredAndOutsideGracePeriod() {
|
||||
return this.hasExpiration && this.expirationWithGracePeriod < new Date();
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue