[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"
|
[providerName]="userOrg.providerName"
|
||||||
></app-org-subscription-hidden>
|
></app-org-subscription-hidden>
|
||||||
|
|
||||||
<ng-container *ngIf="sub && firstLoaded">
|
<ng-container *ngIf="subscription && firstLoaded">
|
||||||
<dl>
|
<dl>
|
||||||
<dt>{{ "billingPlan" | i18n }}</dt>
|
<dt>{{ "billingPlan" | i18n }}</dt>
|
||||||
<dd>{{ sub.plan.name }}</dd>
|
<dd>{{ subscription.planName }}</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>
|
|
||||||
<ng-container *ngIf="billingSyncSetUp">
|
<ng-container *ngIf="billingSyncSetUp">
|
||||||
<dt>{{ "lastLicenseSync" | i18n }}</dt>
|
<dt>{{ "lastLicenseSync" | i18n }}</dt>
|
||||||
<dd>
|
<dd>
|
||||||
{{ lastLicenseSync != null ? (lastLicenseSync | date : "medium") : ("never" | i18n) }}
|
{{ lastLicenseSync != null ? (lastLicenseSync | date : "medium") : ("never" | i18n) }}
|
||||||
</dd>
|
</dd>
|
||||||
</ng-container>
|
</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>
|
</dl>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { Component, OnDestroy, OnInit } from "@angular/core";
|
import { Component, OnDestroy, OnInit } from "@angular/core";
|
||||||
import { FormControl, FormGroup } from "@angular/forms";
|
import { FormControl, FormGroup } from "@angular/forms";
|
||||||
import { ActivatedRoute } from "@angular/router";
|
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 { ModalConfig, ModalService } from "@bitwarden/angular/services/modal.service";
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.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 { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||||
import { OrganizationConnectionResponse } from "@bitwarden/common/admin-console/models/response/organization-connection.response";
|
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 { 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 {
|
import {
|
||||||
BillingSyncKeyComponent,
|
BillingSyncKeyComponent,
|
||||||
|
@ -31,7 +31,7 @@ enum LicenseOptions {
|
||||||
templateUrl: "organization-subscription-selfhost.component.html",
|
templateUrl: "organization-subscription-selfhost.component.html",
|
||||||
})
|
})
|
||||||
export class OrganizationSubscriptionSelfhostComponent implements OnInit, OnDestroy {
|
export class OrganizationSubscriptionSelfhostComponent implements OnInit, OnDestroy {
|
||||||
sub: OrganizationSubscriptionResponse;
|
subscription: SelfHostedOrganizationSubscriptionView;
|
||||||
organizationId: string;
|
organizationId: string;
|
||||||
userOrg: Organization;
|
userOrg: Organization;
|
||||||
|
|
||||||
|
@ -65,6 +65,15 @@ export class OrganizationSubscriptionSelfhostComponent implements OnInit, OnDest
|
||||||
return this.existingBillingSyncConnection?.enabled;
|
return this.existingBillingSyncConnection?.enabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the subscription as expired.
|
||||||
|
*/
|
||||||
|
get showAsExpired() {
|
||||||
|
return this.subscription.hasSeparateGracePeriod
|
||||||
|
? this.subscription.isExpiredWithoutGracePeriod
|
||||||
|
: this.subscription.isExpiredAndOutsideGracePeriod;
|
||||||
|
}
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private modalService: ModalService,
|
private modalService: ModalService,
|
||||||
private messagingService: MessagingService,
|
private messagingService: MessagingService,
|
||||||
|
@ -102,7 +111,10 @@ export class OrganizationSubscriptionSelfhostComponent implements OnInit, OnDest
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
this.userOrg = this.organizationService.get(this.organizationId);
|
this.userOrg = this.organizationService.get(this.organizationId);
|
||||||
if (this.userOrg.canViewSubscription) {
|
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;
|
this.loading = false;
|
||||||
|
@ -159,10 +171,6 @@ export class OrganizationSubscriptionSelfhostComponent implements OnInit, OnDest
|
||||||
return this.existingBillingSyncConnection?.id != null;
|
return this.existingBillingSyncConnection?.id != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
get isExpired() {
|
|
||||||
return this.sub?.expiration != null && new Date(this.sub.expiration) < new Date();
|
|
||||||
}
|
|
||||||
|
|
||||||
get updateMethod() {
|
get updateMethod() {
|
||||||
return this.form.get("updateMethod").value;
|
return this.form.get("updateMethod").value;
|
||||||
}
|
}
|
||||||
|
|
|
@ -6553,6 +6553,18 @@
|
||||||
"billingSyncHelp": {
|
"billingSyncHelp": {
|
||||||
"message": "Billing Sync help"
|
"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": {
|
"uploadLicense": {
|
||||||
"message": "Upload license"
|
"message": "Upload license"
|
||||||
},
|
},
|
||||||
|
|
|
@ -11,6 +11,7 @@ export class OrganizationSubscriptionResponse extends OrganizationResponse {
|
||||||
subscription: BillingSubscriptionResponse;
|
subscription: BillingSubscriptionResponse;
|
||||||
upcomingInvoice: BillingSubscriptionUpcomingInvoiceResponse;
|
upcomingInvoice: BillingSubscriptionUpcomingInvoiceResponse;
|
||||||
expiration: string;
|
expiration: string;
|
||||||
|
expirationWithoutGracePeriod: string;
|
||||||
|
|
||||||
constructor(response: any) {
|
constructor(response: any) {
|
||||||
super(response);
|
super(response);
|
||||||
|
@ -24,5 +25,6 @@ export class OrganizationSubscriptionResponse extends OrganizationResponse {
|
||||||
? null
|
? null
|
||||||
: new BillingSubscriptionUpcomingInvoiceResponse(upcomingInvoice);
|
: new BillingSubscriptionUpcomingInvoiceResponse(upcomingInvoice);
|
||||||
this.expiration = this.getResponseProperty("Expiration");
|
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