diff --git a/apps/web/src/app/billing/accounts/trial-initiation/billing.component.ts b/apps/web/src/app/billing/accounts/trial-initiation/billing.component.ts index 35aee96745..1830b49a45 100644 --- a/apps/web/src/app/billing/accounts/trial-initiation/billing.component.ts +++ b/apps/web/src/app/billing/accounts/trial-initiation/billing.component.ts @@ -1,18 +1,7 @@ import { Component, EventEmitter, Input, Output } from "@angular/core"; -import { UntypedFormBuilder, FormGroup } from "@angular/forms"; -import { Router } from "@angular/router"; +import { FormGroup } from "@angular/forms"; -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 { 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 { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { OrganizationPlansComponent } from "../../settings/organization-plans.component"; @@ -24,36 +13,6 @@ export class BillingComponent extends OrganizationPlansComponent { @Input() orgInfoForm: FormGroup; @Output() previousStep = new EventEmitter(); - constructor( - apiService: ApiService, - i18nService: I18nService, - platformUtilsService: PlatformUtilsService, - cryptoService: CryptoService, - router: Router, - syncService: SyncService, - policyService: PolicyService, - organizationService: OrganizationService, - logService: LogService, - messagingService: MessagingService, - formBuilder: UntypedFormBuilder, - organizationApiService: OrganizationApiServiceAbstraction - ) { - super( - apiService, - i18nService, - platformUtilsService, - cryptoService, - router, - syncService, - policyService, - organizationService, - logService, - messagingService, - formBuilder, - organizationApiService - ); - } - async ngOnInit() { const additionalSeats = this.product == ProductType.Families ? 0 : 1; this.formGroup.patchValue({ diff --git a/apps/web/src/app/billing/organizations/adjust-subscription.component.ts b/apps/web/src/app/billing/organizations/adjust-subscription.component.ts index 012d815b5f..c82f30665d 100644 --- a/apps/web/src/app/billing/organizations/adjust-subscription.component.ts +++ b/apps/web/src/app/billing/organizations/adjust-subscription.component.ts @@ -40,7 +40,7 @@ export class AdjustSubscription { try { const seatAdjustment = this.newSeatCount - this.currentSeatCount; const request = new OrganizationSubscriptionUpdateRequest(seatAdjustment, this.newMaxSeats); - this.formPromise = this.organizationApiService.updateSubscription( + this.formPromise = this.organizationApiService.updatePasswordManagerSeats( this.organizationId, request ); diff --git a/apps/web/src/app/billing/organizations/organization-billing.module.ts b/apps/web/src/app/billing/organizations/organization-billing.module.ts index 8d3dbecd7b..781e19d67b 100644 --- a/apps/web/src/app/billing/organizations/organization-billing.module.ts +++ b/apps/web/src/app/billing/organizations/organization-billing.module.ts @@ -12,7 +12,7 @@ import { OrganizationBillingRoutingModule } from "./organization-billing-routing import { OrganizationBillingTabComponent } from "./organization-billing-tab.component"; import { OrganizationSubscriptionCloudComponent } from "./organization-subscription-cloud.component"; import { OrganizationSubscriptionSelfhostComponent } from "./organization-subscription-selfhost.component"; -import { SecretsManagerEnrollComponent } from "./secrets-manager/enroll.component"; +import { SecretsManagerBillingModule } from "./secrets-manager/sm-billing.module"; import { SubscriptionHiddenComponent } from "./subscription-hidden.component"; @NgModule({ @@ -21,6 +21,7 @@ import { SubscriptionHiddenComponent } from "./subscription-hidden.component"; LooseComponentsModule, OrganizationBillingRoutingModule, UserVerificationModule, + SecretsManagerBillingModule, ], declarations: [ AdjustSubscription, @@ -32,7 +33,6 @@ import { SubscriptionHiddenComponent } from "./subscription-hidden.component"; OrganizationSubscriptionSelfhostComponent, OrganizationSubscriptionCloudComponent, SubscriptionHiddenComponent, - SecretsManagerEnrollComponent, ], }) export class OrganizationBillingModule {} diff --git a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html index 20dc22597f..df29ca3a44 100644 --- a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html +++ b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html @@ -21,7 +21,7 @@ [providerName]="userOrg.providerName" > - + - - + + diff --git a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts index 52245320fe..667c88a531 100644 --- a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts +++ b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts @@ -13,6 +13,8 @@ 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 { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; 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"; @@ -37,6 +39,8 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy showAdjustStorage = false; hasBillingSyncToken: boolean; + showSecretsManagerSubscribe = false; + firstLoaded = false; loading: boolean; @@ -51,7 +55,8 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy private organizationService: OrganizationService, private organizationApiService: OrganizationApiServiceAbstraction, private route: ActivatedRoute, - private dialogService: DialogServiceAbstraction + private dialogService: DialogServiceAbstraction, + private configService: ConfigServiceAbstraction ) {} async ngOnInit() { @@ -105,6 +110,17 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy (i) => i.keyType === OrganizationApiKeyType.BillingSync ); + this.showSecretsManagerSubscribe = + this.userOrg.canEditSubscription && + !this.userOrg.useSecretsManager && + !this.subscription.cancelled && + !this.subscriptionMarkedForCancel; + + // Remove next line when the sm-ga-billing flag is deleted + this.showSecretsManagerSubscribe = + this.showSecretsManagerSubscribe && + (await this.configService.getFeatureFlagBool(FeatureFlag.SecretsManagerBilling)); + this.loading = false; } diff --git a/apps/web/src/app/billing/organizations/secrets-manager/enroll.component.html b/apps/web/src/app/billing/organizations/secrets-manager/enroll.component.html deleted file mode 100644 index de2b6aa566..0000000000 --- a/apps/web/src/app/billing/organizations/secrets-manager/enroll.component.html +++ /dev/null @@ -1,13 +0,0 @@ -
-

{{ "secretsManagerBeta" | i18n }}

-

{{ "secretsManagerSubscriptionDesc" | i18n }}

- - - - {{ "secretsManagerEnable" | i18n }} - - - -
diff --git a/apps/web/src/app/billing/organizations/secrets-manager/enroll.component.ts b/apps/web/src/app/billing/organizations/secrets-manager/enroll.component.ts deleted file mode 100644 index edcedc8d54..0000000000 --- a/apps/web/src/app/billing/organizations/secrets-manager/enroll.component.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { Component, Input, OnInit } from "@angular/core"; -import { FormBuilder } from "@angular/forms"; - -import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; -import { OrganizationEnrollSecretsManagerRequest } from "@bitwarden/common/admin-console/models/request/organization/organization-enroll-secrets-manager.request"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; - -import { flagEnabled } from "../../../../utils/flags"; - -@Component({ - selector: "sm-enroll", - templateUrl: "enroll.component.html", -}) -export class SecretsManagerEnrollComponent implements OnInit { - @Input() enabled: boolean; - @Input() organizationId: string; - - protected formGroup = this.formBuilder.group({ - enabled: [false], - }); - - protected showSecretsManager = false; - - constructor( - private formBuilder: FormBuilder, - private organizationApiService: OrganizationApiServiceAbstraction, - private platformUtilsService: PlatformUtilsService, - private i18nService: I18nService, - private syncService: SyncService - ) { - this.showSecretsManager = flagEnabled("secretsManager"); - } - - ngOnInit(): void { - this.formGroup.setValue({ - enabled: this.enabled, - }); - } - - protected submit = async () => { - this.formGroup.markAllAsTouched(); - - const request = new OrganizationEnrollSecretsManagerRequest(); - request.enabled = this.formGroup.value.enabled; - - await this.organizationApiService.updateEnrollSecretsManager(this.organizationId, request); - await this.syncService.fullSync(true); - this.platformUtilsService.showToast("success", null, this.i18nService.t("subscriptionUpdated")); - }; -} diff --git a/apps/web/src/app/billing/organizations/secrets-manager/index.ts b/apps/web/src/app/billing/organizations/secrets-manager/index.ts new file mode 100644 index 0000000000..fc0bcd3534 --- /dev/null +++ b/apps/web/src/app/billing/organizations/secrets-manager/index.ts @@ -0,0 +1,3 @@ +export * from "./sm-billing.module"; +export * from "./sm-subscribe.component"; +export * from "./sm-subscribe-standalone.component"; diff --git a/apps/web/src/app/billing/organizations/secrets-manager/sm-billing.module.ts b/apps/web/src/app/billing/organizations/secrets-manager/sm-billing.module.ts new file mode 100644 index 0000000000..a46286fc5a --- /dev/null +++ b/apps/web/src/app/billing/organizations/secrets-manager/sm-billing.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from "@angular/core"; + +import { SharedModule } from "../../../shared"; + +import { SecretsManagerSubscribeStandaloneComponent } from "./sm-subscribe-standalone.component"; +import { SecretsManagerSubscribeComponent } from "./sm-subscribe.component"; + +@NgModule({ + imports: [SharedModule], + declarations: [SecretsManagerSubscribeComponent, SecretsManagerSubscribeStandaloneComponent], + exports: [SecretsManagerSubscribeComponent, SecretsManagerSubscribeStandaloneComponent], +}) +export class SecretsManagerBillingModule {} diff --git a/apps/web/src/app/billing/organizations/secrets-manager/sm-subscribe-standalone.component.html b/apps/web/src/app/billing/organizations/secrets-manager/sm-subscribe-standalone.component.html new file mode 100644 index 0000000000..84c74ee428 --- /dev/null +++ b/apps/web/src/app/billing/organizations/secrets-manager/sm-subscribe-standalone.component.html @@ -0,0 +1,8 @@ +
+ +
diff --git a/apps/web/src/app/billing/organizations/secrets-manager/sm-subscribe-standalone.component.ts b/apps/web/src/app/billing/organizations/secrets-manager/sm-subscribe-standalone.component.ts new file mode 100644 index 0000000000..1aa95a6c7a --- /dev/null +++ b/apps/web/src/app/billing/organizations/secrets-manager/sm-subscribe-standalone.component.ts @@ -0,0 +1,42 @@ +import { Component, EventEmitter, Input, Output } from "@angular/core"; +import { FormBuilder } from "@angular/forms"; + +import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { SecretsManagerSubscribeRequest } from "@bitwarden/common/billing/models/request/sm-subscribe.request"; +import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; + +import { secretsManagerSubscribeFormFactory } from "./sm-subscribe.component"; + +@Component({ + selector: "sm-subscribe-standalone", + templateUrl: "sm-subscribe-standalone.component.html", +}) +export class SecretsManagerSubscribeStandaloneComponent { + @Input() plan: PlanResponse; + @Input() organization: Organization; + @Output() onSubscribe = new EventEmitter(); + + formGroup = secretsManagerSubscribeFormFactory(this.formBuilder); + + constructor( + private formBuilder: FormBuilder, + private platformUtilsService: PlatformUtilsService, + private i18nService: I18nService, + private organizationApiService: OrganizationApiServiceAbstraction + ) {} + + submit = async () => { + const request = new SecretsManagerSubscribeRequest(); + request.additionalSmSeats = this.formGroup.value.userSeats; + request.additionalServiceAccounts = this.formGroup.value.additionalServiceAccounts; + + await this.organizationApiService.subscribeToSecretsManager(this.organization.id, request); + + this.platformUtilsService.showToast("success", null, this.i18nService.t("subscriptionUpdated")); + + this.onSubscribe.emit(); + }; +} diff --git a/apps/web/src/app/billing/organizations/secrets-manager/sm-subscribe.component.html b/apps/web/src/app/billing/organizations/secrets-manager/sm-subscribe.component.html new file mode 100644 index 0000000000..e0a1f2066e --- /dev/null +++ b/apps/web/src/app/billing/organizations/secrets-manager/sm-subscribe.component.html @@ -0,0 +1,68 @@ +
+

{{ "moreFromBitwarden" | i18n }}

+
+
+ +
+
+
+

{{ "secretsManagerForPlan" | i18n : planName }}

+
+ {{ "secretsManagerForPlanDesc" | i18n }} +
    +
  • {{ "limitedUsers" | i18n : maxUsers }}
  • +
  • {{ "unlimitedSecrets" | i18n }}
  • +
  • + {{ "projectsIncluded" | i18n : maxProjects }} +
  • + +
  • {{ "unlimitedProjects" | i18n }}
  • +
    +
  • {{ "serviceAccountsIncluded" | i18n : serviceAccountsIncluded }}
  • +
  • + {{ + "additionalServiceAccountCost" | i18n : (monthlyCostPerServiceAccount | currency : "$") + }} +
  • +
+
+ +
+ + {{ "costPerUser" | i18n : (monthlyCostPerUser | currency : "$") }} /{{ "month" | i18n }} + + + {{ "freeForever" | i18n }} + +
+ + + + {{ "addSecretsManager" | i18n }} + {{ "addSecretsManagerUpgradeDesc" | i18n }} + + +
+ + {{ "userSeats" | i18n }} + + {{ "userSeatsHowManyDesc" | i18n }} + + + + {{ "additionalServiceAccounts" | i18n }} + + {{ + "additionalServiceAccountsDesc" + | i18n : serviceAccountsIncluded : (monthlyCostPerServiceAccount | currency : "$") + }} + + + +
+
+
diff --git a/apps/web/src/app/billing/organizations/secrets-manager/sm-subscribe.component.ts b/apps/web/src/app/billing/organizations/secrets-manager/sm-subscribe.component.ts new file mode 100644 index 0000000000..ef6b73584a --- /dev/null +++ b/apps/web/src/app/billing/organizations/secrets-manager/sm-subscribe.component.ts @@ -0,0 +1,104 @@ +import { Component, Input, OnDestroy, OnInit } from "@angular/core"; +import { FormBuilder, FormGroup, Validators } from "@angular/forms"; +import { Subject, startWith, takeUntil } from "rxjs"; + +import { ControlsOf } from "@bitwarden/angular/types/controls-of"; +import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response"; +import { ProductType } from "@bitwarden/common/enums"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; + +import { SecretsManagerLogo } from "../../../../../../../bitwarden_license/bit-web/src/app/secrets-manager/layout/secrets-manager-logo"; + +export interface SecretsManagerSubscription { + enabled: boolean; + userSeats: number; + additionalServiceAccounts: number; +} + +export const secretsManagerSubscribeFormFactory = ( + formBuilder: FormBuilder +): FormGroup> => + formBuilder.group({ + enabled: [false], + userSeats: [1, [Validators.required, Validators.min(1), Validators.max(100000)]], + additionalServiceAccounts: [ + 0, + [Validators.required, Validators.min(0), Validators.max(100000)], + ], + }); + +@Component({ + selector: "sm-subscribe", + templateUrl: "sm-subscribe.component.html", +}) +export class SecretsManagerSubscribeComponent implements OnInit, OnDestroy { + @Input() formGroup: FormGroup>; + @Input() upgradeOrganization: boolean; + @Input() showSubmitButton = false; + @Input() selectedPlan: PlanResponse; + + logo = SecretsManagerLogo; + productTypes = ProductType; + + private destroy$ = new Subject(); + + constructor(private i18nService: I18nService) {} + + ngOnInit() { + this.formGroup.controls.enabled.valueChanges + .pipe(startWith(this.formGroup.value.enabled), takeUntil(this.destroy$)) + .subscribe((enabled) => { + if (enabled) { + this.formGroup.controls.userSeats.enable(); + this.formGroup.controls.additionalServiceAccounts.enable(); + } else { + this.formGroup.controls.userSeats.disable(); + this.formGroup.controls.additionalServiceAccounts.disable(); + } + }); + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } + + get product() { + return this.selectedPlan.product; + } + + get planName() { + switch (this.product) { + case ProductType.Free: + return this.i18nService.t("free2PersonOrganization"); + case ProductType.Teams: + return this.i18nService.t("planNameTeams"); + case ProductType.Enterprise: + return this.i18nService.t("planNameEnterprise"); + } + } + + get serviceAccountsIncluded() { + return this.selectedPlan.baseServiceAccount; + } + + get monthlyCostPerServiceAccount() { + return this.selectedPlan.isAnnual + ? this.selectedPlan.additionalPricePerServiceAccount / 12 + : this.selectedPlan.additionalPricePerServiceAccount; + } + + get maxUsers() { + return this.selectedPlan.maxUsers; + } + + get maxProjects() { + return this.selectedPlan.maxProjects; + } + + get monthlyCostPerUser() { + return this.selectedPlan.isAnnual + ? this.selectedPlan.seatPrice / 12 + : this.selectedPlan.seatPrice; + } +} diff --git a/apps/web/src/app/billing/settings/organization-plans.component.html b/apps/web/src/app/billing/settings/organization-plans.component.html index a7698f1bf2..dea4e21c9d 100644 --- a/apps/web/src/app/billing/settings/organization-plans.component.html +++ b/apps/web/src/app/billing/settings/organization-plans.component.html @@ -28,7 +28,7 @@ (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate - *ngIf="!loading && !selfHosted && this.plans" + *ngIf="!loading && !selfHosted && this.passwordManagerPlans && this.secretsManagerPlans" class="tw-pt-6" > {{ "freeForever" | i18n }} -
+

{{ "users" | i18n }}

@@ -230,7 +230,8 @@ {{ "users" | i18n }}: {{ formGroup.controls["additionalSeats"].value || 0 }} × {{ selectablePlan.seatPrice / 12 | currency : "$" }} × 12 - {{ "monthAbbr" | i18n }} = {{ seatTotal(selectablePlan) | currency : "$" }} /{{ + {{ "monthAbbr" | i18n }} = + {{ seatTotal(selectablePlan, formGroup.value.additionalSeats) | currency : "$" }} /{{ "year" | i18n }} @@ -256,7 +257,9 @@ {{ "users" | i18n }}: {{ formGroup.controls["additionalSeats"].value || 0 }} × {{ selectablePlan.seatPrice | currency : "$" }} {{ "monthAbbr" | i18n }} = - {{ seatTotal(selectablePlan) | currency : "$" }} /{{ "month" | i18n }} + {{ seatTotal(selectablePlan, formGroup.value.additionalSeats) | currency : "$" }} /{{ + "month" | i18n + }} {{ "additionalStorageGb" | i18n }}: @@ -268,8 +271,21 @@
-
-

+

+ + +
+ +
+ + +
+

{{ (createOrganization ? "paymentInformation" : "billingInformation") | i18n }}

@@ -279,8 +295,12 @@
- {{ "planPrice" | i18n }}: {{ subtotal | currency : "USD $" }} + {{ "passwordManagerPlanPrice" | i18n }}: {{ passwordManagerSubtotal | currency : "USD $" }}
+ + {{ "secretsManagerPlanPrice" | i18n }}: {{ secretsManagerSubtotal | currency : "USD $" }} +
+
{{ "estimatedTax" | i18n }}: {{ taxCharges | currency : "USD $" }} diff --git a/apps/web/src/app/billing/settings/organization-plans.component.ts b/apps/web/src/app/billing/settings/organization-plans.component.ts index 363f7bc46b..23a38c654a 100644 --- a/apps/web/src/app/billing/settings/organization-plans.component.ts +++ b/apps/web/src/app/billing/settings/organization-plans.component.ts @@ -7,7 +7,7 @@ import { Output, ViewChild, } from "@angular/core"; -import { UntypedFormBuilder, Validators } from "@angular/forms"; +import { FormBuilder, Validators } from "@angular/forms"; import { Router } from "@angular/router"; import { Subject, takeUntil } from "rxjs"; @@ -21,8 +21,11 @@ import { OrganizationKeysRequest } from "@bitwarden/common/admin-console/models/ 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 { PaymentMethodType, PlanType } from "@bitwarden/common/billing/enums"; +import { BitwardenProductType } from "@bitwarden/common/billing/enums/bitwarden-product-type"; import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response"; import { ProductType } from "@bitwarden/common/enums"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; 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"; @@ -32,6 +35,8 @@ import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; +import { secretsManagerSubscribeFormFactory } from "../organizations/secrets-manager/sm-subscribe.component"; + import { PaymentComponent } from "./payment.component"; import { TaxInfoComponent } from "./tax-info.component"; @@ -82,6 +87,9 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { singleOrgPolicyAppliesToActiveUser = false; isInTrialFlow = false; discount = 0; + showSecretsManagerSubscribe: boolean; + + secretsManagerSubscription = secretsManagerSubscribeFormFactory(this.formBuilder); formGroup = this.formBuilder.group({ name: [""], @@ -94,9 +102,11 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { businessName: [""], plan: [this.plan], product: [this.product], + secretsManager: this.secretsManagerSubscription, }); - plans: PlanResponse[]; + passwordManagerPlans: PlanResponse[]; + secretsManagerPlans: PlanResponse[]; private destroy$ = new Subject(); @@ -111,8 +121,9 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { private organizationService: OrganizationService, private logService: LogService, private messagingService: MessagingService, - private formBuilder: UntypedFormBuilder, - private organizationApiService: OrganizationApiServiceAbstraction + private formBuilder: FormBuilder, + private organizationApiService: OrganizationApiServiceAbstraction, + private configService: ConfigServiceAbstraction ) { this.selfHosted = platformUtilsService.isSelfHost(); } @@ -120,7 +131,13 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { async ngOnInit() { if (!this.selfHosted) { const plans = await this.apiService.getPlans(); - this.plans = plans.data; + this.passwordManagerPlans = plans.data.filter( + (plan) => plan.bitwardenProduct === BitwardenProductType.PasswordManager + ); + this.secretsManagerPlans = plans.data.filter( + (plan) => plan.bitwardenProduct === BitwardenProductType.SecretsManager + ); + if (this.product === ProductType.Enterprise || this.product === ProductType.Teams) { this.formGroup.controls.businessOwned.setValue(true); } @@ -131,12 +148,9 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { this.changedOwnedBusiness(); } - if (!this.createOrganization || this.acceptingSponsorship) { - this.formGroup.controls.product.setValue(ProductType.Families); - this.changedProduct(); - } - - if (this.createOrganization) { + if (!this.createOrganization) { + this.upgradeFlowPrefillForm(); + } else { this.formGroup.controls.name.addValidators([Validators.required, Validators.maxLength(50)]); this.formGroup.controls.billingEmail.addValidators(Validators.required); } @@ -148,6 +162,11 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { this.singleOrgPolicyAppliesToActiveUser = policyAppliesToActiveUser; }); + this.showSecretsManagerSubscribe = await this.configService.getFeatureFlagBool( + FeatureFlag.SecretsManagerBilling, + false + ); + this.loading = false; } @@ -165,7 +184,15 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { } get selectedPlan() { - return this.plans.find((plan) => plan.type === this.formGroup.controls.plan.value); + 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() { @@ -173,7 +200,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { } get selectableProducts() { - let validPlans = this.plans.filter((plan) => plan.type !== PlanType.Custom); + let validPlans = this.passwordManagerPlans.filter((plan) => plan.type !== PlanType.Custom); if (this.formGroup.controls.businessOwned.value) { validPlans = validPlans.filter((plan) => plan.canBeUsedByBusiness); @@ -191,7 +218,9 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { ); if (this.acceptingSponsorship) { - const familyPlan = this.plans.find((plan) => plan.type === PlanType.FamiliesAnnually); + const familyPlan = this.passwordManagerPlans.find( + (plan) => plan.type === PlanType.FamiliesAnnually + ); this.discount = familyPlan.basePrice; validPlans = [familyPlan]; } @@ -200,7 +229,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { } get selectablePlans() { - return this.plans?.filter( + return this.passwordManagerPlans?.filter( (plan) => !plan.legacyYear && !plan.disabled && plan.product === this.formGroup.controls.product.value ); @@ -231,21 +260,32 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { ); } - seatTotal(plan: PlanResponse): number { + seatTotal(plan: PlanResponse, seats: number): number { if (!plan.hasAdditionalSeatsOption) { return 0; } - return plan.seatPrice * Math.abs(this.formGroup.controls.additionalSeats.value || 0); + return plan.seatPrice * Math.abs(seats || 0); } - get subtotal() { + additionalServiceAccountTotal(plan: PlanResponse): number { + if (!plan.hasAdditionalServiceAccountOption) { + return 0; + } + + return ( + plan.additionalPricePerServiceAccount * + Math.abs(this.secretsManagerForm.value.additionalServiceAccounts || 0) + ); + } + + get passwordManagerSubtotal() { let subTotal = this.selectedPlan.basePrice; if ( this.selectedPlan.hasAdditionalSeatsOption && this.formGroup.controls.additionalSeats.value ) { - subTotal += this.seatTotal(this.selectedPlan); + subTotal += this.seatTotal(this.selectedPlan, this.formGroup.value.additionalSeats); } if ( this.selectedPlan.hasAdditionalStorageOption && @@ -262,18 +302,39 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { return subTotal - this.discount; } + get secretsManagerSubtotal() { + const plan = this.selectedSecretsManagerPlan; + const formValues = this.secretsManagerForm.value; + + if (!this.planOffersSecretsManager || !formValues.enabled) { + return 0; + } + + let subTotal = plan.basePrice; + if (plan.hasAdditionalSeatsOption && formValues.userSeats) { + subTotal += this.seatTotal(plan, formValues.userSeats); + } + + if (plan.hasAdditionalStorageOption && formValues.additionalServiceAccounts) { + subTotal += this.additionalServiceAccountTotal(this.selectedPlan); + } + + return subTotal; + } + get freeTrial() { return this.selectedPlan.trialPeriodDays != null; } get taxCharges() { return this.taxComponent != null && this.taxComponent.taxRate != null - ? (this.taxComponent.taxRate / 100) * this.subtotal + ? (this.taxComponent.taxRate / 100) * + (this.passwordManagerSubtotal + this.secretsManagerSubtotal) : 0; } get total() { - return this.subtotal + this.taxCharges || 0; + return this.passwordManagerSubtotal + this.secretsManagerSubtotal + this.taxCharges || 0; } get paymentDesc() { @@ -286,6 +347,14 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { } } + get secretsManagerForm() { + return this.formGroup.controls.secretsManager; + } + + get planOffersSecretsManager() { + return this.selectedSecretsManagerPlan != null; + } + changedProduct() { this.formGroup.controls.plan.setValue(this.selectablePlans[0].type); if (!this.selectedPlan.hasPremiumAccessOption) { @@ -303,6 +372,14 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { ) { this.formGroup.controls.additionalSeats.setValue(1); } + + if (this.planOffersSecretsManager) { + this.secretsManagerForm.enable(); + } else { + this.secretsManagerForm.disable(); + } + + this.secretsManagerForm.updateValueAndValidity(); } changedOwnedBusiness() { @@ -407,6 +484,9 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { request.billingAddressCountry = this.taxComponent.taxInfo.country; request.billingAddressPostalCode = this.taxComponent.taxInfo.postalCode; + // Secrets Manager + this.buildSecretsManagerRequest(request); + // Retrieve org info to backfill pub/priv key if necessary const org = await this.organizationService.get(this.organizationId); if (!org.hasPublicAndPrivateKeys) { @@ -462,6 +542,9 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { } } + // Secrets Manager + this.buildSecretsManagerRequest(request); + if (this.providerId) { const providerRequest = new ProviderOrganizationCreateRequest( this.formGroup.controls.clientOwnerEmail.value, @@ -517,4 +600,40 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { 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.hasAdditionalSeatsOption) { + request.additionalSmSeats = formValues.userSeats; + } + + if (this.selectedSecretsManagerPlan.hasAdditionalServiceAccountOption) { + request.additionalServiceAccounts = formValues.additionalServiceAccounts; + } + } + + private upgradeFlowPrefillForm() { + if (this.acceptingSponsorship) { + this.formGroup.controls.product.setValue(ProductType.Families); + this.changedProduct(); + return; + } + + // If they already have SM enabled, bump them up to Teams and enable SM to maintain this access + const organization = this.organizationService.get(this.organizationId); + if (organization.useSecretsManager) { + this.formGroup.controls.product.setValue(ProductType.Teams); + this.secretsManagerForm.controls.enabled.setValue(true); + this.changedProduct(); + } + } } diff --git a/apps/web/src/app/shared/loose-components.module.ts b/apps/web/src/app/shared/loose-components.module.ts index c17cf5c76b..cf9a43915a 100644 --- a/apps/web/src/app/shared/loose-components.module.ts +++ b/apps/web/src/app/shared/loose-components.module.ts @@ -50,6 +50,7 @@ import { UpdatePasswordComponent } from "../auth/update-password.component"; import { UpdateTempPasswordComponent } from "../auth/update-temp-password.component"; import { VerifyEmailTokenComponent } from "../auth/verify-email-token.component"; import { VerifyRecoverDeleteComponent } from "../auth/verify-recover-delete.component"; +import { SecretsManagerBillingModule } from "../billing/organizations/secrets-manager/sm-billing.module"; import { AddCreditComponent } from "../billing/settings/add-credit.component"; import { AdjustPaymentComponent } from "../billing/settings/adjust-payment.component"; import { BillingHistoryViewComponent } from "../billing/settings/billing-history-view.component"; @@ -123,6 +124,9 @@ import { SharedModule } from "./shared.module"; ChangeKdfModule, DynamicAvatarComponent, AccountFingerprintComponent, + + // To be removed when OrganizationPlansComponent is moved to its own module (see AC-1453) + SecretsManagerBillingModule, ], declarations: [ PremiumBadgeComponent, diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 72b3165e03..330937ecd9 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -6906,6 +6906,82 @@ "removeMembersWithoutMasterPasswordWarning": { "message": "Removing members who do not have master passwords without setting one for them may restrict access to their full account." }, + "secretsManagerForPlan": { + "message": "Secrets Manager for $PLAN$", + "placeholders": { + "plan": { + "content": "$1", + "example": "Teams" + } + } + }, + "secretsManagerForPlanDesc": { + "message": "For engineering and DevOps teams to manage secrets throughout the software development lifecycle." + }, + "free2PersonOrganization": { + "message": "Free 2-person Organizations" + }, + "unlimitedSecrets": { + "message": "Unlimited secrets" + }, + "unlimitedProjects": { + "message": "Unlimited projects" + }, + "projectsIncluded": { + "message": "$COUNT$ projects included", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "serviceAccountsIncluded": { + "message": "$COUNT$ service accounts included", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "additionalServiceAccountCost": { + "message": "$COST$ per month for additional service accounts", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "addSecretsManager": { + "message": "Add Secrets Manager" + }, + "addSecretsManagerUpgradeDesc": { + "message": "Add Secrets Manager to your upgraded plan to maintain access to any secrets created with your previous plan." + }, + "additionalServiceAccounts": { + "message": "Additional service accounts" + }, + "additionalServiceAccountsDesc": { + "message": "Your plan comes with $COUNT$ service accounts. You can add additional service accounts for $COST$ per month.", + "placeholders": { + "count": { + "content": "$1", + "example": "50" + }, + "cost": { + "content": "$2", + "example": "$0.50" + } + } + }, + "passwordManagerPlanPrice": { + "message": "Password Manager plan price" + }, + "secretsManagerPlanPrice": { + "message": "Secrets Manager plan price" + }, "passwordManager": { "message": "Password Manager" }, @@ -6913,3 +6989,4 @@ "message": "Free Organization" } } + diff --git a/libs/common/src/admin-console/abstractions/organization/organization-api.service.abstraction.ts b/libs/common/src/admin-console/abstractions/organization/organization-api.service.abstraction.ts index 93f4de2ffd..c6ce62f232 100644 --- a/libs/common/src/admin-console/abstractions/organization/organization-api.service.abstraction.ts +++ b/libs/common/src/admin-console/abstractions/organization/organization-api.service.abstraction.ts @@ -6,6 +6,7 @@ import { OrganizationSsoResponse } from "../../../auth/models/response/organizat import { OrganizationSubscriptionUpdateRequest } from "../../../billing/models/request/organization-subscription-update.request"; import { OrganizationTaxInfoUpdateRequest } from "../../../billing/models/request/organization-tax-info-update.request"; import { PaymentRequest } from "../../../billing/models/request/payment.request"; +import { SecretsManagerSubscribeRequest } from "../../../billing/models/request/sm-subscribe.request"; import { BillingResponse } from "../../../billing/models/response/billing.response"; import { OrganizationSubscriptionResponse } from "../../../billing/models/response/organization-subscription.response"; import { PaymentResponse } from "../../../billing/models/response/payment.response"; @@ -16,7 +17,6 @@ import { StorageRequest } from "../../../models/request/storage.request"; import { VerifyBankRequest } from "../../../models/request/verify-bank.request"; import { ListResponse } from "../../../models/response/list.response"; import { OrganizationApiKeyType } from "../../enums"; -import { OrganizationEnrollSecretsManagerRequest } from "../../models/request/organization/organization-enroll-secrets-manager.request"; import { OrganizationCreateRequest } from "../../models/request/organization-create.request"; import { OrganizationKeysRequest } from "../../models/request/organization-keys.request"; import { OrganizationUpdateRequest } from "../../models/request/organization-update.request"; @@ -37,7 +37,10 @@ export class OrganizationApiServiceAbstraction { save: (id: string, request: OrganizationUpdateRequest) => Promise; updatePayment: (id: string, request: PaymentRequest) => Promise; upgrade: (id: string, request: OrganizationUpgradeRequest) => Promise; - updateSubscription: (id: string, request: OrganizationSubscriptionUpdateRequest) => Promise; + updatePasswordManagerSeats: ( + id: string, + request: OrganizationSubscriptionUpdateRequest + ) => Promise; updateSeats: (id: string, request: SeatRequest) => Promise; updateStorage: (id: string, request: StorageRequest) => Promise; verifyBank: (id: string, request: VerifyBankRequest) => Promise; @@ -60,8 +63,5 @@ export class OrganizationApiServiceAbstraction { getSso: (id: string) => Promise; updateSso: (id: string, request: OrganizationSsoRequest) => Promise; selfHostedSyncLicense: (id: string) => Promise; - updateEnrollSecretsManager: ( - id: string, - request: OrganizationEnrollSecretsManagerRequest - ) => Promise; + subscribeToSecretsManager: (id: string, request: SecretsManagerSubscribeRequest) => Promise; } diff --git a/libs/common/src/admin-console/models/request/organization-create.request.ts b/libs/common/src/admin-console/models/request/organization-create.request.ts index 616c37c00c..729cf45365 100644 --- a/libs/common/src/admin-console/models/request/organization-create.request.ts +++ b/libs/common/src/admin-console/models/request/organization-create.request.ts @@ -23,4 +23,8 @@ export class OrganizationCreateRequest { billingAddressState: string; billingAddressPostalCode: string; billingAddressCountry: string; + + useSecretsManager: boolean; + additionalSmSeats: number; + additionalServiceAccounts: number; } diff --git a/libs/common/src/admin-console/models/request/organization-upgrade.request.ts b/libs/common/src/admin-console/models/request/organization-upgrade.request.ts index bf0eb5f47f..eba897f31b 100644 --- a/libs/common/src/admin-console/models/request/organization-upgrade.request.ts +++ b/libs/common/src/admin-console/models/request/organization-upgrade.request.ts @@ -11,4 +11,8 @@ export class OrganizationUpgradeRequest { billingAddressCountry: string; billingAddressPostalCode: string; keys: OrganizationKeysRequest; + + useSecretsManager: boolean; + additionalSmSeats: number; + additionalServiceAccounts: number; } diff --git a/libs/common/src/admin-console/models/request/organization/organization-enroll-secrets-manager.request.ts b/libs/common/src/admin-console/models/request/organization/organization-enroll-secrets-manager.request.ts deleted file mode 100644 index a213b07bba..0000000000 --- a/libs/common/src/admin-console/models/request/organization/organization-enroll-secrets-manager.request.ts +++ /dev/null @@ -1,3 +0,0 @@ -export class OrganizationEnrollSecretsManagerRequest { - enabled: boolean; -} diff --git a/libs/common/src/admin-console/models/response/organization.response.ts b/libs/common/src/admin-console/models/response/organization.response.ts index 2c056ee287..6bd339a64d 100644 --- a/libs/common/src/admin-console/models/response/organization.response.ts +++ b/libs/common/src/admin-console/models/response/organization.response.ts @@ -13,6 +13,7 @@ export class OrganizationResponse extends BaseResponse { businessTaxNumber: string; billingEmail: string; plan: PlanResponse; + secretsManagerPlan: PlanResponse; planType: PlanType; seats: number; maxAutoscaleSeats: number; @@ -39,8 +40,14 @@ export class OrganizationResponse extends BaseResponse { this.businessCountry = this.getResponseProperty("BusinessCountry"); this.businessTaxNumber = this.getResponseProperty("BusinessTaxNumber"); this.billingEmail = this.getResponseProperty("BillingEmail"); + const plan = this.getResponseProperty("Plan"); this.plan = plan == null ? null : new PlanResponse(plan); + + const secretsManagerPlan = this.getResponseProperty("SecretsManagerPlan"); + this.secretsManagerPlan = + secretsManagerPlan == null ? null : new PlanResponse(secretsManagerPlan); + this.planType = this.getResponseProperty("PlanType"); this.seats = this.getResponseProperty("Seats"); this.maxAutoscaleSeats = this.getResponseProperty("MaxAutoscaleSeats"); diff --git a/libs/common/src/admin-console/services/organization/organization-api.service.ts b/libs/common/src/admin-console/services/organization/organization-api.service.ts index 3a1d355524..503aeb3820 100644 --- a/libs/common/src/admin-console/services/organization/organization-api.service.ts +++ b/libs/common/src/admin-console/services/organization/organization-api.service.ts @@ -7,6 +7,7 @@ import { OrganizationSsoResponse } from "../../../auth/models/response/organizat import { OrganizationSubscriptionUpdateRequest } from "../../../billing/models/request/organization-subscription-update.request"; import { OrganizationTaxInfoUpdateRequest } from "../../../billing/models/request/organization-tax-info-update.request"; import { PaymentRequest } from "../../../billing/models/request/payment.request"; +import { SecretsManagerSubscribeRequest } from "../../../billing/models/request/sm-subscribe.request"; import { BillingResponse } from "../../../billing/models/response/billing.response"; import { OrganizationSubscriptionResponse } from "../../../billing/models/response/organization-subscription.response"; import { PaymentResponse } from "../../../billing/models/response/payment.response"; @@ -19,7 +20,6 @@ import { ListResponse } from "../../../models/response/list.response"; import { SyncService } from "../../../vault/abstractions/sync/sync.service.abstraction"; import { OrganizationApiServiceAbstraction } from "../../abstractions/organization/organization-api.service.abstraction"; import { OrganizationApiKeyType } from "../../enums"; -import { OrganizationEnrollSecretsManagerRequest } from "../../models/request/organization/organization-enroll-secrets-manager.request"; import { OrganizationCreateRequest } from "../../models/request/organization-create.request"; import { OrganizationKeysRequest } from "../../models/request/organization-keys.request"; import { OrganizationUpdateRequest } from "../../models/request/organization-update.request"; @@ -120,7 +120,7 @@ export class OrganizationApiService implements OrganizationApiServiceAbstraction return new PaymentResponse(r); } - async updateSubscription( + async updatePasswordManagerSeats( id: string, request: OrganizationSubscriptionUpdateRequest ): Promise { @@ -294,13 +294,16 @@ export class OrganizationApiService implements OrganizationApiServiceAbstraction ); } - async updateEnrollSecretsManager(id: string, request: OrganizationEnrollSecretsManagerRequest) { - await this.apiService.send( + async subscribeToSecretsManager( + id: string, + request: SecretsManagerSubscribeRequest + ): Promise { + return await this.apiService.send( "POST", - "/organizations/" + id + "/enroll-secrets-manager", + "/organizations/" + id + "/subscribe-secrets-manager", request, true, - true + false ); } } diff --git a/libs/common/src/billing/enums/bitwarden-product-type.ts b/libs/common/src/billing/enums/bitwarden-product-type.ts new file mode 100644 index 0000000000..76b0899fd9 --- /dev/null +++ b/libs/common/src/billing/enums/bitwarden-product-type.ts @@ -0,0 +1,4 @@ +export enum BitwardenProductType { + PasswordManager = 0, + SecretsManager = 1, +} diff --git a/libs/common/src/billing/models/request/sm-subscribe.request.ts b/libs/common/src/billing/models/request/sm-subscribe.request.ts new file mode 100644 index 0000000000..581d3007c8 --- /dev/null +++ b/libs/common/src/billing/models/request/sm-subscribe.request.ts @@ -0,0 +1,4 @@ +export class SecretsManagerSubscribeRequest { + additionalSmSeats: number; + additionalServiceAccounts: number; +} diff --git a/libs/common/src/billing/models/response/plan.response.ts b/libs/common/src/billing/models/response/plan.response.ts index 8368849b86..e67ed35e44 100644 --- a/libs/common/src/billing/models/response/plan.response.ts +++ b/libs/common/src/billing/models/response/plan.response.ts @@ -1,10 +1,12 @@ import { ProductType } from "../../../enums"; import { BaseResponse } from "../../../models/response/base.response"; import { PlanType } from "../../enums"; +import { BitwardenProductType } from "../../enums/bitwarden-product-type"; export class PlanResponse extends BaseResponse { type: PlanType; product: ProductType; + bitwardenProduct: BitwardenProductType; name: string; isAnnual: boolean; nameLocalizationKey: string; @@ -48,6 +50,13 @@ export class PlanResponse extends BaseResponse { additionalStoragePricePerGb: number; premiumAccessOptionPrice: number; + // SM only + additionalPricePerServiceAccount: number; + baseServiceAccount: number; + maxServiceAccount: number; + hasAdditionalServiceAccountOption: boolean; + maxProjects: number; + constructor(response: any) { super(response); this.type = this.getResponseProperty("Type"); @@ -90,5 +99,16 @@ export class PlanResponse extends BaseResponse { this.seatPrice = this.getResponseProperty("SeatPrice"); this.additionalStoragePricePerGb = this.getResponseProperty("AdditionalStoragePricePerGb"); this.premiumAccessOptionPrice = this.getResponseProperty("PremiumAccessOptionPrice"); + + this.bitwardenProduct = this.getResponseProperty("BitwardenProduct"); + this.additionalPricePerServiceAccount = this.getResponseProperty( + "AdditionalPricePerServiceAccount" + ); + this.baseServiceAccount = this.getResponseProperty("BaseServiceAccount"); + this.maxServiceAccount = this.getResponseProperty("MaxServiceAccount"); + this.hasAdditionalServiceAccountOption = this.getResponseProperty( + "HasAdditionalServiceAccountOption" + ); + this.maxProjects = this.getResponseProperty("MaxProjects"); } } diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index e8a05911b9..fb155f54e2 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -2,4 +2,5 @@ export enum FeatureFlag { DisplayEuEnvironmentFlag = "display-eu-environment", DisplayLowKdfIterationWarningFlag = "display-kdf-iteration-warning", TrustedDeviceEncryption = "trusted-device-encryption", + SecretsManagerBilling = "sm-ga-billing", } diff --git a/libs/common/src/services/api.service.ts b/libs/common/src/services/api.service.ts index ad7c134889..9c098632b1 100644 --- a/libs/common/src/services/api.service.ts +++ b/libs/common/src/services/api.service.ts @@ -881,7 +881,7 @@ export class ApiService implements ApiServiceAbstraction { // Plan APIs async getPlans(): Promise> { - const r = await this.send("GET", "/plans/", null, false, true); + const r = await this.send("GET", "/plans/all", null, false, true); return new ListResponse(r, PlanResponse); }