import { Component, EventEmitter, Input, OnDestroy, OnInit, Output, ViewChild, } from "@angular/core"; import { FormBuilder, Validators } from "@angular/forms"; import { Router } from "@angular/router"; import { Subject, takeUntil } from "rxjs"; 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 { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { OrganizationCreateRequest } from "@bitwarden/common/admin-console/models/request/organization-create.request"; import { OrganizationKeysRequest } from "@bitwarden/common/admin-console/models/request/organization-keys.request"; import { OrganizationUpgradeRequest } from "@bitwarden/common/admin-console/models/request/organization-upgrade.request"; import { ProviderOrganizationCreateRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-organization-create.request"; import { ProviderResponse } from "@bitwarden/common/admin-console/models/response/provider/provider.response"; import { PaymentMethodType, PlanType } from "@bitwarden/common/billing/enums"; import { PaymentRequest } from "@bitwarden/common/billing/models/request/payment.request"; import { BillingResponse } from "@bitwarden/common/billing/models/response/billing.response"; import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.response"; import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response"; import { ProductType } from "@bitwarden/common/enums"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { OrgKey } from "@bitwarden/common/types/key"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { OrganizationCreateModule } from "../../admin-console/organizations/create/organization-create.module"; import { BillingSharedModule, secretsManagerSubscribeFormFactory } from "../shared"; import { PaymentComponent } from "../shared/payment.component"; import { TaxInfoComponent } from "../shared/tax-info.component"; interface OnSuccessArgs { organizationId: string; } const Allowed2020PlansForLegacyProviders = [ PlanType.TeamsMonthly2020, PlanType.TeamsAnnually2020, PlanType.EnterpriseAnnually2020, PlanType.EnterpriseMonthly2020, ]; @Component({ selector: "app-organization-plans", templateUrl: "organization-plans.component.html", standalone: true, imports: [BillingSharedModule, OrganizationCreateModule], }) export class OrganizationPlansComponent implements OnInit, OnDestroy { @ViewChild(PaymentComponent) paymentComponent: PaymentComponent; @ViewChild(TaxInfoComponent) taxComponent: TaxInfoComponent; @Input() organizationId: string; @Input() showFree = true; @Input() showCancel = false; @Input() acceptingSponsorship = false; @Input() currentPlan: PlanResponse; @Input() get product(): ProductType { return this._product; } set product(product: ProductType) { this._product = product; this.formGroup?.controls?.product?.setValue(product); } private _product = ProductType.Free; @Input() get plan(): PlanType { return this._plan; } set plan(plan: PlanType) { this._plan = plan; this.formGroup?.controls?.plan?.setValue(plan); } private _plan = PlanType.Free; @Input() providerId?: string; @Output() onSuccess = new EventEmitter(); @Output() onCanceled = new EventEmitter(); @Output() onTrialBillingSuccess = new EventEmitter(); loading = true; selfHosted = false; productTypes = ProductType; formPromise: Promise; singleOrgPolicyAppliesToActiveUser = false; isInTrialFlow = false; discount = 0; secretsManagerSubscription = secretsManagerSubscribeFormFactory(this.formBuilder); formGroup = this.formBuilder.group({ name: [""], billingEmail: ["", [Validators.email]], businessOwned: [false], premiumAccessAddon: [false], additionalStorage: [0, [Validators.min(0), Validators.max(99)]], additionalSeats: [0, [Validators.min(0), Validators.max(100000)]], clientOwnerEmail: ["", [Validators.email]], plan: [this.plan], product: [this.product], secretsManager: this.secretsManagerSubscription, }); passwordManagerPlans: PlanResponse[]; secretsManagerPlans: PlanResponse[]; organization: Organization; sub: OrganizationSubscriptionResponse; billing: BillingResponse; provider: ProviderResponse; private destroy$ = new Subject(); constructor( private apiService: ApiService, private i18nService: I18nService, private platformUtilsService: PlatformUtilsService, private cryptoService: CryptoService, private router: Router, private syncService: SyncService, private policyService: PolicyService, private organizationService: OrganizationService, private logService: LogService, private messagingService: MessagingService, private formBuilder: FormBuilder, private organizationApiService: OrganizationApiServiceAbstraction, private providerApiService: ProviderApiServiceAbstraction, ) { this.selfHosted = platformUtilsService.isSelfHost(); } async ngOnInit() { if (this.organizationId) { this.organization = await this.organizationService.get(this.organizationId); this.billing = await this.organizationApiService.getBilling(this.organizationId); this.sub = await this.organizationApiService.getSubscription(this.organizationId); } if (!this.selfHosted) { const plans = await this.apiService.getPlans(); this.passwordManagerPlans = plans.data.filter((plan) => !!plan.PasswordManager); this.secretsManagerPlans = plans.data.filter((plan) => !!plan.SecretsManager); if (this.product === ProductType.Enterprise || this.product === ProductType.Teams) { this.formGroup.controls.businessOwned.setValue(true); } } if (this.currentPlan && this.currentPlan.product !== ProductType.Enterprise) { const upgradedPlan = this.passwordManagerPlans.find((plan) => this.currentPlan.product === ProductType.Free ? plan.type === PlanType.FamiliesAnnually : plan.upgradeSortOrder == this.currentPlan.upgradeSortOrder + 1, ); this.plan = upgradedPlan.type; this.product = upgradedPlan.product; } if (this.hasProvider) { this.formGroup.controls.businessOwned.setValue(true); this.changedOwnedBusiness(); this.provider = await this.providerApiService.getProvider(this.providerId); const providerDefaultPlan = this.passwordManagerPlans.find( (plan) => plan.type === PlanType.TeamsAnnually, ); this.plan = providerDefaultPlan.type; this.product = providerDefaultPlan.product; } if (!this.createOrganization) { this.upgradeFlowPrefillForm(); } else { this.formGroup.controls.name.addValidators([Validators.required, Validators.maxLength(50)]); this.formGroup.controls.billingEmail.addValidators(Validators.required); } this.policyService .policyAppliesToActiveUser$(PolicyType.SingleOrg) .pipe(takeUntil(this.destroy$)) .subscribe((policyAppliesToActiveUser) => { this.singleOrgPolicyAppliesToActiveUser = policyAppliesToActiveUser; }); if (!this.selfHosted) { this.changedProduct(); } this.loading = false; } ngOnDestroy() { this.destroy$.next(); this.destroy$.complete(); } get singleOrgPolicyBlock() { return this.singleOrgPolicyAppliesToActiveUser && !this.hasProvider; } get createOrganization() { return this.organizationId == null; } get upgradeRequiresPaymentMethod() { return ( this.organization?.planProductType === ProductType.Free && !this.showFree && !this.billing?.paymentSource ); } get selectedPlan() { return this.passwordManagerPlans.find( (plan) => plan.type === this.formGroup.controls.plan.value, ); } get selectedSecretsManagerPlan() { return this.secretsManagerPlans.find( (plan) => plan.type === this.formGroup.controls.plan.value, ); } get selectedPlanInterval() { return this.selectedPlan.isAnnual ? "year" : "month"; } isProviderQualifiedFor2020Plan() { const targetDate = new Date("2023-11-06"); if (!this.provider || !this.provider.creationDate) { return false; } const creationDate = new Date(this.provider.creationDate); return creationDate < targetDate; } get selectableProducts() { if (this.acceptingSponsorship) { const familyPlan = this.passwordManagerPlans.find( (plan) => plan.type === PlanType.FamiliesAnnually, ); this.discount = familyPlan.PasswordManager.basePrice; return [familyPlan]; } const businessOwnedIsChecked = this.formGroup.controls.businessOwned.value; const result = this.passwordManagerPlans.filter( (plan) => plan.type !== PlanType.Custom && (!businessOwnedIsChecked || plan.canBeUsedByBusiness) && (this.showFree || plan.product !== ProductType.Free) && (plan.isAnnual || plan.product === ProductType.Free || plan.product === ProductType.TeamsStarter) && (!this.currentPlan || this.currentPlan.upgradeSortOrder < plan.upgradeSortOrder) && (!this.hasProvider || plan.product !== ProductType.TeamsStarter) && ((!this.isProviderQualifiedFor2020Plan() && this.planIsEnabled(plan)) || (this.isProviderQualifiedFor2020Plan() && Allowed2020PlansForLegacyProviders.includes(plan.type))), ); result.sort((planA, planB) => planA.displaySortOrder - planB.displaySortOrder); return result; } get selectablePlans() { const selectedProductType = this.formGroup.controls.product.value; const result = this.passwordManagerPlans?.filter( (plan) => plan.product === selectedProductType && ((!this.isProviderQualifiedFor2020Plan() && this.planIsEnabled(plan)) || (this.isProviderQualifiedFor2020Plan() && Allowed2020PlansForLegacyProviders.includes(plan.type))), ) || []; result.sort((planA, planB) => planA.displaySortOrder - planB.displaySortOrder); return result; } get hasProvider() { return this.providerId != null; } additionalStoragePriceMonthly(selectedPlan: PlanResponse) { if (!selectedPlan.isAnnual) { return selectedPlan.PasswordManager.additionalStoragePricePerGb; } return selectedPlan.PasswordManager.additionalStoragePricePerGb / 12; } seatPriceMonthly(selectedPlan: PlanResponse) { if (!selectedPlan.isAnnual) { return selectedPlan.PasswordManager.seatPrice; } return selectedPlan.PasswordManager.seatPrice / 12; } additionalStorageTotal(plan: PlanResponse): number { if (!plan.PasswordManager.hasAdditionalStorageOption) { return 0; } return ( plan.PasswordManager.additionalStoragePricePerGb * Math.abs(this.formGroup.controls.additionalStorage.value || 0) ); } passwordManagerSeatTotal(plan: PlanResponse, seats: number): number { if (!plan.PasswordManager.hasAdditionalSeatsOption) { return 0; } return plan.PasswordManager.seatPrice * Math.abs(seats || 0); } secretsManagerSeatTotal(plan: PlanResponse, seats: number): number { if (!plan.SecretsManager.hasAdditionalSeatsOption) { return 0; } return plan.SecretsManager.seatPrice * Math.abs(seats || 0); } additionalServiceAccountTotal(plan: PlanResponse): number { if (!plan.SecretsManager.hasAdditionalServiceAccountOption) { return 0; } return ( plan.SecretsManager.additionalPricePerServiceAccount * Math.abs(this.secretsManagerForm.value.additionalServiceAccounts || 0) ); } get passwordManagerSubtotal() { let subTotal = this.selectedPlan.PasswordManager.basePrice; if ( this.selectedPlan.PasswordManager.hasAdditionalSeatsOption && this.formGroup.controls.additionalSeats.value ) { subTotal += this.passwordManagerSeatTotal( this.selectedPlan, this.formGroup.value.additionalSeats, ); } if ( this.selectedPlan.PasswordManager.hasAdditionalStorageOption && this.formGroup.controls.additionalStorage.value ) { subTotal += this.additionalStorageTotal(this.selectedPlan); } if ( this.selectedPlan.PasswordManager.hasPremiumAccessOption && this.formGroup.controls.premiumAccessAddon.value ) { subTotal += this.selectedPlan.PasswordManager.premiumAccessOptionPrice; } return subTotal - this.discount; } get secretsManagerSubtotal() { const plan = this.selectedSecretsManagerPlan; const formValues = this.secretsManagerForm.value; if (!this.planOffersSecretsManager || !formValues.enabled) { return 0; } return ( plan.SecretsManager.basePrice + this.secretsManagerSeatTotal(plan, formValues.userSeats) + this.additionalServiceAccountTotal(plan) ); } get freeTrial() { return this.selectedPlan.trialPeriodDays != null; } get taxCharges() { return this.taxComponent != null && this.taxComponent.taxRate != null ? (this.taxComponent.taxRate / 100) * (this.passwordManagerSubtotal + this.secretsManagerSubtotal) : 0; } get total() { return this.passwordManagerSubtotal + this.secretsManagerSubtotal + this.taxCharges || 0; } get paymentDesc() { if (this.acceptingSponsorship) { return this.i18nService.t("paymentSponsored"); } else if (this.freeTrial && this.createOrganization) { return this.i18nService.t("paymentChargedWithTrial"); } else { return this.i18nService.t("paymentCharged", this.i18nService.t(this.selectedPlanInterval)); } } get secretsManagerForm() { return this.formGroup.controls.secretsManager; } get planOffersSecretsManager() { return this.selectedSecretsManagerPlan != null; } changedProduct() { const selectedPlan = this.selectablePlans[0]; this.setPlanType(selectedPlan.type); this.handlePremiumAddonAccess(selectedPlan.PasswordManager.hasPremiumAccessOption); this.handleAdditionalStorage(selectedPlan.PasswordManager.hasAdditionalStorageOption); this.handleAdditionalSeats(selectedPlan.PasswordManager.hasAdditionalSeatsOption); this.handleSecretsManagerForm(); } setPlanType(planType: PlanType) { this.formGroup.controls.plan.setValue(planType); } handlePremiumAddonAccess(hasPremiumAccessOption: boolean) { this.formGroup.controls.premiumAccessAddon.setValue(!hasPremiumAccessOption); } handleAdditionalStorage(selectedPlanHasAdditionalStorageOption: boolean) { if (!selectedPlanHasAdditionalStorageOption || !this.currentPlan) { this.formGroup.controls.additionalStorage.setValue(0); return; } if (this.organization?.maxStorageGb) { this.formGroup.controls.additionalStorage.setValue( this.organization.maxStorageGb - this.currentPlan.PasswordManager.baseStorageGb, ); } } handleAdditionalSeats(selectedPlanHasAdditionalSeatsOption: boolean) { if (!selectedPlanHasAdditionalSeatsOption) { this.formGroup.controls.additionalSeats.setValue(0); return; } if (this.currentPlan && !this.currentPlan.PasswordManager.hasAdditionalSeatsOption) { this.formGroup.controls.additionalSeats.setValue(this.currentPlan.PasswordManager.baseSeats); return; } if (this.organization) { this.formGroup.controls.additionalSeats.setValue(this.organization.seats); return; } this.formGroup.controls.additionalSeats.setValue(1); } handleSecretsManagerForm() { if (this.planOffersSecretsManager) { this.secretsManagerForm.enable(); } if (this.organization?.useSecretsManager) { this.secretsManagerForm.controls.enabled.setValue(true); } if (this.secretsManagerForm.controls.enabled.value) { this.secretsManagerForm.controls.userSeats.setValue(this.sub?.smSeats || 1); this.secretsManagerForm.controls.additionalServiceAccounts.setValue( this.sub?.smServiceAccounts - this.currentPlan.SecretsManager?.baseServiceAccount || 0, ); } this.secretsManagerForm.updateValueAndValidity(); } changedOwnedBusiness() { if (!this.formGroup.controls.businessOwned.value || this.selectedPlan.canBeUsedByBusiness) { return; } this.formGroup.controls.product.setValue(ProductType.TeamsStarter); this.formGroup.controls.plan.setValue(PlanType.TeamsStarter); this.changedProduct(); } changedCountry() { this.paymentComponent.hideBank = this.taxComponent.taxInfo.country !== "US"; // Bank Account payments are only available for US customers if ( this.paymentComponent.hideBank && this.paymentComponent.method === PaymentMethodType.BankAccount ) { this.paymentComponent.method = PaymentMethodType.Card; this.paymentComponent.changeMethod(); } } cancel() { this.onCanceled.emit(); } async submit() { if (this.singleOrgPolicyBlock) { return; } try { const doSubmit = async (): Promise => { let orgId: string = null; if (this.createOrganization) { const orgKey = await this.cryptoService.makeOrgKey(); const key = orgKey[0].encryptedString; const collection = await this.cryptoService.encrypt( this.i18nService.t("defaultCollection"), orgKey[1], ); const collectionCt = collection.encryptedString; const orgKeys = await this.cryptoService.makeKeyPair(orgKey[1]); if (this.selfHosted) { orgId = await this.createSelfHosted(key, collectionCt, orgKeys); } else { orgId = await this.createCloudHosted(key, collectionCt, orgKeys, orgKey[1]); } this.platformUtilsService.showToast( "success", this.i18nService.t("organizationCreated"), this.i18nService.t("organizationReadyToGo"), ); } else { orgId = await this.updateOrganization(orgId); this.platformUtilsService.showToast( "success", null, this.i18nService.t("organizationUpgraded"), ); } await this.apiService.refreshIdentityToken(); await this.syncService.fullSync(true); if (!this.acceptingSponsorship && !this.isInTrialFlow) { // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises this.router.navigate(["/organizations/" + orgId]); } if (this.isInTrialFlow) { this.onTrialBillingSuccess.emit({ orgId: orgId, subLabelText: this.billingSubLabelText(), }); } return orgId; }; this.formPromise = doSubmit(); const organizationId = await this.formPromise; this.onSuccess.emit({ organizationId: organizationId }); // TODO: No one actually listening to this message? this.messagingService.send("organizationCreated", { organizationId }); } catch (e) { this.logService.error(e); } } private async updateOrganization(orgId: string) { const request = new OrganizationUpgradeRequest(); request.additionalSeats = this.formGroup.controls.additionalSeats.value; request.additionalStorageGb = this.formGroup.controls.additionalStorage.value; request.premiumAccessAddon = this.selectedPlan.PasswordManager.hasPremiumAccessOption && this.formGroup.controls.premiumAccessAddon.value; request.planType = this.selectedPlan.type; request.billingAddressCountry = this.taxComponent.taxInfo.country; request.billingAddressPostalCode = this.taxComponent.taxInfo.postalCode; // Secrets Manager this.buildSecretsManagerRequest(request); if (this.upgradeRequiresPaymentMethod) { const tokenResult = await this.paymentComponent.createPaymentToken(); const paymentRequest = new PaymentRequest(); paymentRequest.paymentToken = tokenResult[0]; paymentRequest.paymentMethodType = tokenResult[1]; paymentRequest.country = this.taxComponent.taxInfo.country; paymentRequest.postalCode = this.taxComponent.taxInfo.postalCode; await this.organizationApiService.updatePayment(this.organizationId, paymentRequest); } // Backfill pub/priv key if necessary if (!this.organization.hasPublicAndPrivateKeys) { const orgShareKey = await this.cryptoService.getOrgKey(this.organizationId); const orgKeys = await this.cryptoService.makeKeyPair(orgShareKey); request.keys = new OrganizationKeysRequest(orgKeys[0], orgKeys[1].encryptedString); } const result = await this.organizationApiService.upgrade(this.organizationId, request); if (!result.success && result.paymentIntentClientSecret != null) { await this.paymentComponent.handleStripeCardPayment(result.paymentIntentClientSecret, null); } return this.organizationId; } private async createCloudHosted( key: string, collectionCt: string, orgKeys: [string, EncString], orgKey: SymmetricCryptoKey, ) { const request = new OrganizationCreateRequest(); request.key = key; request.collectionName = collectionCt; request.name = this.formGroup.controls.name.value; request.billingEmail = this.formGroup.controls.billingEmail.value; request.initiationPath = "New organization creation in-product"; request.keys = new OrganizationKeysRequest(orgKeys[0], orgKeys[1].encryptedString); if (this.selectedPlan.type === PlanType.Free) { request.planType = PlanType.Free; } else { const tokenResult = await this.paymentComponent.createPaymentToken(); request.paymentToken = tokenResult[0]; request.paymentMethodType = tokenResult[1]; request.additionalSeats = this.formGroup.controls.additionalSeats.value; request.additionalStorageGb = this.formGroup.controls.additionalStorage.value; request.premiumAccessAddon = this.selectedPlan.PasswordManager.hasPremiumAccessOption && this.formGroup.controls.premiumAccessAddon.value; request.planType = this.selectedPlan.type; request.billingAddressPostalCode = this.taxComponent.taxInfo.postalCode; request.billingAddressCountry = this.taxComponent.taxInfo.country; if (this.taxComponent.taxInfo.includeTaxId) { request.taxIdNumber = this.taxComponent.taxInfo.taxId; request.billingAddressLine1 = this.taxComponent.taxInfo.line1; request.billingAddressLine2 = this.taxComponent.taxInfo.line2; request.billingAddressCity = this.taxComponent.taxInfo.city; request.billingAddressState = this.taxComponent.taxInfo.state; } } // Secrets Manager this.buildSecretsManagerRequest(request); if (this.hasProvider) { const providerRequest = new ProviderOrganizationCreateRequest( this.formGroup.controls.clientOwnerEmail.value, request, ); const providerKey = await this.cryptoService.getProviderKey(this.providerId); providerRequest.organizationCreateRequest.key = ( await this.cryptoService.encrypt(orgKey.key, providerKey) ).encryptedString; const orgId = ( await this.apiService.postProviderCreateOrganization(this.providerId, providerRequest) ).organizationId; return orgId; } else { return (await this.organizationApiService.create(request)).id; } } private async createSelfHosted(key: string, collectionCt: string, orgKeys: [string, EncString]) { const fileEl = document.getElementById("file") as HTMLInputElement; const files = fileEl.files; if (files == null || files.length === 0) { throw new Error(this.i18nService.t("selectFile")); } const fd = new FormData(); fd.append("license", files[0]); fd.append("key", key); fd.append("collectionName", collectionCt); const response = await this.organizationApiService.createLicense(fd); const orgId = response.id; await this.apiService.refreshIdentityToken(); // Org Keys live outside of the OrganizationLicense - add the keys to the org here const request = new OrganizationKeysRequest(orgKeys[0], orgKeys[1].encryptedString); await this.organizationApiService.updateKeys(orgId, request); return orgId; } private billingSubLabelText(): string { const selectedPlan = this.selectedPlan; const price = selectedPlan.PasswordManager.basePrice === 0 ? selectedPlan.PasswordManager.seatPrice : selectedPlan.PasswordManager.basePrice; let text = ""; if (selectedPlan.isAnnual) { text += `${this.i18nService.t("annual")} ($${price}/${this.i18nService.t("yr")})`; } else { text += `${this.i18nService.t("monthly")} ($${price}/${this.i18nService.t("monthAbbr")})`; } return text; } private buildSecretsManagerRequest( request: OrganizationCreateRequest | OrganizationUpgradeRequest, ): void { const formValues = this.secretsManagerForm.value; request.useSecretsManager = this.planOffersSecretsManager && formValues.enabled; if (!request.useSecretsManager) { return; } if (this.selectedSecretsManagerPlan.SecretsManager.hasAdditionalSeatsOption) { request.additionalSmSeats = formValues.userSeats; } if (this.selectedSecretsManagerPlan.SecretsManager.hasAdditionalServiceAccountOption) { request.additionalServiceAccounts = formValues.additionalServiceAccounts; } } private upgradeFlowPrefillForm() { if (this.acceptingSponsorship) { this.formGroup.controls.product.setValue(ProductType.Families); this.changedProduct(); return; } if (this.currentPlan && this.currentPlan.product !== ProductType.Enterprise) { const upgradedPlan = this.passwordManagerPlans.find((plan) => this.currentPlan.product === ProductType.Free ? plan.type === PlanType.FamiliesAnnually : plan.upgradeSortOrder == this.currentPlan.upgradeSortOrder + 1, ); this.plan = upgradedPlan.type; this.product = upgradedPlan.product; this.changedProduct(); } } private planIsEnabled(plan: PlanResponse) { return !plan.disabled && !plan.legacyYear; } }