mirror of
https://github.com/bitwarden/browser
synced 2026-01-04 17:43:39 +00:00
370 lines
10 KiB
TypeScript
370 lines
10 KiB
TypeScript
import { Component, OnDestroy, OnInit } from "@angular/core";
|
|
import { ActivatedRoute } from "@angular/router";
|
|
import { concatMap, Subject, takeUntil } from "rxjs";
|
|
|
|
import { DialogServiceAbstraction, SimpleDialogType } from "@bitwarden/angular/services/dialog";
|
|
import { ModalConfig, ModalService } from "@bitwarden/angular/services/modal.service";
|
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
|
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
|
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
|
import { OrganizationApiKeyType } from "@bitwarden/common/admin-console/enums";
|
|
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
|
import { PlanType } from "@bitwarden/common/billing/enums";
|
|
import { BitwardenProductType } from "@bitwarden/common/billing/enums/bitwarden-product-type.enum";
|
|
import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.response";
|
|
import { BillingSubscriptionItemResponse } from "@bitwarden/common/billing/models/response/subscription.response";
|
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
|
|
|
import {
|
|
BillingSyncApiKeyComponent,
|
|
BillingSyncApiModalData,
|
|
} from "./billing-sync-api-key.component";
|
|
|
|
@Component({
|
|
selector: "app-org-subscription-cloud",
|
|
templateUrl: "organization-subscription-cloud.component.html",
|
|
})
|
|
export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy {
|
|
sub: OrganizationSubscriptionResponse;
|
|
lineItems: BillingSubscriptionItemResponse[] = [];
|
|
organizationId: string;
|
|
userOrg: Organization;
|
|
showChangePlan = false;
|
|
showDownloadLicense = false;
|
|
adjustStorageAdd = true;
|
|
showAdjustStorage = false;
|
|
hasBillingSyncToken: boolean;
|
|
|
|
firstLoaded = false;
|
|
loading: boolean;
|
|
|
|
private destroy$ = new Subject<void>();
|
|
|
|
constructor(
|
|
private apiService: ApiService,
|
|
private platformUtilsService: PlatformUtilsService,
|
|
private i18nService: I18nService,
|
|
private logService: LogService,
|
|
private modalService: ModalService,
|
|
private organizationService: OrganizationService,
|
|
private organizationApiService: OrganizationApiServiceAbstraction,
|
|
private route: ActivatedRoute,
|
|
private dialogService: DialogServiceAbstraction
|
|
) {}
|
|
|
|
async ngOnInit() {
|
|
if (this.route.snapshot.queryParamMap.get("upgrade")) {
|
|
this.changePlan();
|
|
}
|
|
|
|
this.route.params
|
|
.pipe(
|
|
concatMap(async (params) => {
|
|
this.organizationId = params.organizationId;
|
|
await this.load();
|
|
this.firstLoaded = true;
|
|
}),
|
|
takeUntil(this.destroy$)
|
|
)
|
|
.subscribe();
|
|
}
|
|
|
|
productName(product: BitwardenProductType) {
|
|
switch (product) {
|
|
case BitwardenProductType.PasswordManager:
|
|
return this.i18nService.t("passwordManager");
|
|
case BitwardenProductType.SecretsManager:
|
|
return this.i18nService.t("secretsManager");
|
|
default:
|
|
return this.i18nService.t("passwordManager");
|
|
}
|
|
}
|
|
|
|
ngOnDestroy() {
|
|
this.destroy$.next();
|
|
this.destroy$.complete();
|
|
}
|
|
|
|
async load() {
|
|
if (this.loading) {
|
|
return;
|
|
}
|
|
this.loading = true;
|
|
this.userOrg = this.organizationService.get(this.organizationId);
|
|
if (this.userOrg.canViewSubscription) {
|
|
this.sub = await this.organizationApiService.getSubscription(this.organizationId);
|
|
this.lineItems = this.sub?.subscription?.items?.sort(sortSubscriptionItems) ?? [];
|
|
}
|
|
|
|
const apiKeyResponse = await this.organizationApiService.getApiKeyInformation(
|
|
this.organizationId
|
|
);
|
|
this.hasBillingSyncToken = apiKeyResponse.data.some(
|
|
(i) => i.keyType === OrganizationApiKeyType.BillingSync
|
|
);
|
|
|
|
this.loading = false;
|
|
}
|
|
|
|
get subscription() {
|
|
return this.sub != null ? this.sub.subscription : null;
|
|
}
|
|
|
|
get nextInvoice() {
|
|
return this.sub != null ? this.sub.upcomingInvoice : null;
|
|
}
|
|
|
|
get isExpired() {
|
|
const nextInvoice = this.nextInvoice;
|
|
|
|
if (nextInvoice == null) {
|
|
return false;
|
|
}
|
|
|
|
return new Date(nextInvoice.date).getTime() < Date.now();
|
|
}
|
|
|
|
get storagePercentage() {
|
|
return this.sub != null && this.sub.maxStorageGb
|
|
? +(100 * (this.sub.storageGb / this.sub.maxStorageGb)).toFixed(2)
|
|
: 0;
|
|
}
|
|
|
|
get storageProgressWidth() {
|
|
return this.storagePercentage < 5 ? 5 : 0;
|
|
}
|
|
|
|
get billingInterval() {
|
|
const monthly = !this.sub.plan.isAnnual;
|
|
return monthly ? "month" : "year";
|
|
}
|
|
|
|
get storageGbPrice() {
|
|
return this.sub.plan.additionalStoragePricePerGb;
|
|
}
|
|
|
|
get seatPrice() {
|
|
return this.sub.plan.seatPrice;
|
|
}
|
|
|
|
get seats() {
|
|
return this.sub.seats;
|
|
}
|
|
|
|
get maxAutoscaleSeats() {
|
|
return this.sub.maxAutoscaleSeats;
|
|
}
|
|
|
|
get canAdjustSeats() {
|
|
return this.sub.plan.hasAdditionalSeatsOption;
|
|
}
|
|
|
|
get isAdmin() {
|
|
return this.userOrg.isAdmin;
|
|
}
|
|
|
|
get isSponsoredSubscription(): boolean {
|
|
return this.sub.subscription?.items.some((i) => i.sponsoredSubscriptionItem);
|
|
}
|
|
|
|
get canDownloadLicense() {
|
|
return (
|
|
(this.sub.planType !== PlanType.Free && this.subscription == null) ||
|
|
(this.subscription != null && !this.subscription.cancelled)
|
|
);
|
|
}
|
|
|
|
get canManageBillingSync() {
|
|
return (
|
|
this.sub.planType === PlanType.EnterpriseAnnually ||
|
|
this.sub.planType === PlanType.EnterpriseMonthly ||
|
|
this.sub.planType === PlanType.EnterpriseAnnually2019 ||
|
|
this.sub.planType === PlanType.EnterpriseMonthly2019
|
|
);
|
|
}
|
|
|
|
get subscriptionDesc() {
|
|
if (this.sub.planType === PlanType.Free) {
|
|
return this.i18nService.t("subscriptionFreePlan", this.sub.seats.toString());
|
|
} else if (
|
|
this.sub.planType === PlanType.FamiliesAnnually ||
|
|
this.sub.planType === PlanType.FamiliesAnnually2019
|
|
) {
|
|
if (this.isSponsoredSubscription) {
|
|
return this.i18nService.t("subscriptionSponsoredFamiliesPlan", this.sub.seats.toString());
|
|
} else {
|
|
return this.i18nService.t("subscriptionFamiliesPlan", this.sub.seats.toString());
|
|
}
|
|
} else if (this.sub.maxAutoscaleSeats === this.sub.seats && this.sub.seats != null) {
|
|
return this.i18nService.t("subscriptionMaxReached", this.sub.seats.toString());
|
|
} else if (this.sub.maxAutoscaleSeats == null) {
|
|
return this.i18nService.t("subscriptionUserSeatsUnlimitedAutoscale");
|
|
} else {
|
|
return this.i18nService.t(
|
|
"subscriptionUserSeatsLimitedAutoscale",
|
|
this.sub.maxAutoscaleSeats.toString()
|
|
);
|
|
}
|
|
}
|
|
|
|
get subscriptionMarkedForCancel() {
|
|
return (
|
|
this.subscription != null && !this.subscription.cancelled && this.subscription.cancelAtEndDate
|
|
);
|
|
}
|
|
|
|
cancel = async () => {
|
|
if (this.loading) {
|
|
return;
|
|
}
|
|
|
|
const confirmed = await this.dialogService.openSimpleDialog({
|
|
title: { key: "cancelSubscription" },
|
|
content: { key: "cancelConfirmation" },
|
|
type: SimpleDialogType.WARNING,
|
|
});
|
|
|
|
if (!confirmed) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await this.organizationApiService.cancel(this.organizationId);
|
|
this.platformUtilsService.showToast(
|
|
"success",
|
|
null,
|
|
this.i18nService.t("canceledSubscription")
|
|
);
|
|
this.load();
|
|
} catch (e) {
|
|
this.logService.error(e);
|
|
}
|
|
};
|
|
|
|
reinstate = async () => {
|
|
if (this.loading) {
|
|
return;
|
|
}
|
|
|
|
const confirmed = await this.dialogService.openSimpleDialog({
|
|
title: { key: "reinstateSubscription" },
|
|
content: { key: "reinstateConfirmation" },
|
|
type: SimpleDialogType.WARNING,
|
|
});
|
|
|
|
if (!confirmed) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await this.organizationApiService.reinstate(this.organizationId);
|
|
this.platformUtilsService.showToast("success", null, this.i18nService.t("reinstated"));
|
|
this.load();
|
|
} catch (e) {
|
|
this.logService.error(e);
|
|
}
|
|
};
|
|
|
|
async changePlan() {
|
|
this.showChangePlan = !this.showChangePlan;
|
|
}
|
|
|
|
closeChangePlan() {
|
|
this.showChangePlan = false;
|
|
}
|
|
|
|
downloadLicense() {
|
|
this.showDownloadLicense = !this.showDownloadLicense;
|
|
}
|
|
|
|
async manageBillingSync() {
|
|
const modalConfig: ModalConfig<BillingSyncApiModalData> = {
|
|
data: {
|
|
organizationId: this.organizationId,
|
|
hasBillingToken: this.hasBillingSyncToken,
|
|
},
|
|
};
|
|
const modalRef = this.modalService.open(BillingSyncApiKeyComponent, modalConfig);
|
|
|
|
modalRef.onClosed
|
|
.pipe(
|
|
concatMap(async () => {
|
|
this.load();
|
|
}),
|
|
takeUntil(this.destroy$)
|
|
)
|
|
.subscribe();
|
|
}
|
|
|
|
closeDownloadLicense() {
|
|
this.showDownloadLicense = false;
|
|
}
|
|
|
|
subscriptionAdjusted() {
|
|
this.load();
|
|
}
|
|
|
|
adjustStorage(add: boolean) {
|
|
this.adjustStorageAdd = add;
|
|
this.showAdjustStorage = true;
|
|
}
|
|
|
|
closeStorage(load: boolean) {
|
|
this.showAdjustStorage = false;
|
|
if (load) {
|
|
this.load();
|
|
}
|
|
}
|
|
|
|
removeSponsorship = async () => {
|
|
const confirmed = await this.dialogService.openSimpleDialog({
|
|
title: { key: "removeSponsorship" },
|
|
content: { key: "removeSponsorshipConfirmation" },
|
|
acceptButtonText: { key: "remove" },
|
|
type: SimpleDialogType.WARNING,
|
|
});
|
|
|
|
if (!confirmed) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await this.apiService.deleteRemoveSponsorship(this.organizationId);
|
|
this.platformUtilsService.showToast(
|
|
"success",
|
|
null,
|
|
this.i18nService.t("removeSponsorshipSuccess")
|
|
);
|
|
await this.load();
|
|
} catch (e) {
|
|
this.logService.error(e);
|
|
}
|
|
};
|
|
|
|
get showChangePlanButton() {
|
|
return this.subscription == null && this.sub.planType === PlanType.Free && !this.showChangePlan;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Helper to sort subscription items by product type and then by addon status
|
|
*/
|
|
function sortSubscriptionItems(
|
|
a: BillingSubscriptionItemResponse,
|
|
b: BillingSubscriptionItemResponse
|
|
) {
|
|
if (a.bitwardenProduct == b.bitwardenProduct) {
|
|
if (a.addonSubscriptionItem == b.addonSubscriptionItem) {
|
|
return 0;
|
|
}
|
|
// sort addon items to the bottom
|
|
if (a.addonSubscriptionItem) {
|
|
return 1;
|
|
}
|
|
return -1;
|
|
}
|
|
return a.bitwardenProduct - b.bitwardenProduct;
|
|
}
|