- {{ "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 363f7bc46bc..7d9f76708ea 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";
@@ -20,9 +20,11 @@ import { OrganizationCreateRequest } from "@bitwarden/common/admin-console/model
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 { PaymentMethodType, PlanType } from "@bitwarden/common/billing/enums";
+import { BitwardenProductType, PaymentMethodType, PlanType } from "@bitwarden/common/billing/enums";
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 +34,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";
@@ -51,24 +55,29 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
@Input() showFree = true;
@Input() showCancel = false;
@Input() acceptingSponsorship = false;
+
@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
();
@@ -82,6 +91,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 +106,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 +125,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 +135,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 +152,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 +166,11 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
this.singleOrgPolicyAppliesToActiveUser = policyAppliesToActiveUser;
});
+ this.showSecretsManagerSubscribe = await this.configService.getFeatureFlagBool(
+ FeatureFlag.SecretsManagerBilling,
+ false
+ );
+
this.loading = false;
}
@@ -165,7 +188,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 +204,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 +222,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 +233,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 +264,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 +306,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 +351,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 +376,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 +488,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 +546,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 +604,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 afbc047a321..38a4b40c6e3 100644
--- a/apps/web/src/app/shared/loose-components.module.ts
+++ b/apps/web/src/app/shared/loose-components.module.ts
@@ -51,6 +51,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";
@@ -125,6 +126,9 @@ import { SharedModule } from "./shared.module";
DynamicAvatarComponent,
EnvironmentSelectorModule,
AccountFingerprintComponent,
+
+ // To be removed when OrganizationPlansComponent is moved to its own module (see AC-1453)
+ SecretsManagerBillingModule,
],
declarations: [
AcceptEmergencyComponent,
diff --git a/apps/web/src/app/shared/shared.module.ts b/apps/web/src/app/shared/shared.module.ts
index ace5160a53b..350ecce8dad 100644
--- a/apps/web/src/app/shared/shared.module.ts
+++ b/apps/web/src/app/shared/shared.module.ts
@@ -23,6 +23,7 @@ import {
LinkModule,
MenuModule,
MultiSelectModule,
+ ProgressModule,
RadioButtonModule,
SelectModule,
TableModule,
@@ -69,6 +70,7 @@ import "./locales";
LinkModule,
MenuModule,
MultiSelectModule,
+ ProgressModule,
RadioButtonModule,
TableModule,
TabsModule,
@@ -103,6 +105,7 @@ import "./locales";
LinkModule,
MenuModule,
MultiSelectModule,
+ ProgressModule,
RadioButtonModule,
SelectModule,
TableModule,
diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json
index 3d6e9f6018f..a8e08399e69 100644
--- a/apps/web/src/locales/en/messages.json
+++ b/apps/web/src/locales/en/messages.json
@@ -3323,6 +3323,9 @@
"limitSubscriptionDesc": {
"message": "Set a seat limit for your subscription. Once this limit is reached, you will not be able to invite new members."
},
+ "limitSmSubscriptionDesc": {
+ "message": "Set a seat limit for your Secrets Manger subscription. Once this limit is reached, you will not be able to invite new members."
+ },
"maxSeatLimit": {
"message": "Seat Limit (optional)",
"description": "Upper limit of seats to allow through autoscaling"
@@ -6617,12 +6620,12 @@
"changeKdfLoggedOutWarning": {
"message": "Proceeding will log you out of all active sessions. You will need to log back in and complete two-step login setup. We recommend exporting your vault before changing your encryption settings to prevent data loss."
},
- "secretsManagerBeta": {
- "message": "Secrets Manager Beta"
- },
"secretsManager": {
"message": "Secrets Manager"
},
+ "secretsManagerBeta": {
+ "message": "Secrets Manager Beta"
+ },
"secretsManagerAccessDescription": {
"message": "Activate user access to Secrets Manager."
},
@@ -6955,5 +6958,99 @@
},
"selectedRegionFlag": {
"message": "Selected region flag"
+ },
+ "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"
+ },
+ "freeOrganization": {
+ "message": "Free Organization"
+ },
+ "limitServiceAccounts": {
+ "message": "Limit service accounts (optional)"
+ },
+ "limitServiceAccountsDesc": {
+ "message": "Set a limit for your service accounts. Once this limit is reached, you will not be able to create new service accounts."
+ },
+ "serviceAccountLimit": {
+ "message": "Service account limit (optional)"
+ },
+ "maxServiceAccountCost": {
+ "message": "Max potential service account cost"
}
}
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 93f4de2ffdf..b6778cf61e6 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
@@ -3,9 +3,11 @@ import { OrganizationSsoRequest } from "../../../auth/models/request/organizatio
import { SecretVerificationRequest } from "../../../auth/models/request/secret-verification.request";
import { ApiKeyResponse } from "../../../auth/models/response/api-key.response";
import { OrganizationSsoResponse } from "../../../auth/models/response/organization-sso.response";
+import { OrganizationSmSubscriptionUpdateRequest } from "../../../billing/models/request/organization-sm-subscription-update.request";
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 +18,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 +38,14 @@ 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;
+ updateSecretsManagerSubscription: (
+ id: string,
+ request: OrganizationSmSubscriptionUpdateRequest
+ ) => Promise;
updateSeats: (id: string, request: SeatRequest) => Promise;
updateStorage: (id: string, request: StorageRequest) => Promise;
verifyBank: (id: string, request: VerifyBankRequest) => Promise;
@@ -60,8 +68,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/data/organization.data.ts b/libs/common/src/admin-console/models/data/organization.data.ts
index 36ebb0f3df8..10f5e9f2625 100644
--- a/libs/common/src/admin-console/models/data/organization.data.ts
+++ b/libs/common/src/admin-console/models/data/organization.data.ts
@@ -22,6 +22,7 @@ export class OrganizationData {
useCustomPermissions: boolean;
useResetPassword: boolean;
useSecretsManager: boolean;
+ usePasswordManager: boolean;
useActivateAutofillPolicy: boolean;
selfHost: boolean;
usersGetPremium: boolean;
@@ -74,6 +75,7 @@ export class OrganizationData {
this.useCustomPermissions = response.useCustomPermissions;
this.useResetPassword = response.useResetPassword;
this.useSecretsManager = response.useSecretsManager;
+ this.usePasswordManager = response.usePasswordManager;
this.useActivateAutofillPolicy = response.useActivateAutofillPolicy;
this.selfHost = response.selfHost;
this.usersGetPremium = response.usersGetPremium;
diff --git a/libs/common/src/admin-console/models/domain/organization.ts b/libs/common/src/admin-console/models/domain/organization.ts
index 11e0ccc0349..89751eb3e8d 100644
--- a/libs/common/src/admin-console/models/domain/organization.ts
+++ b/libs/common/src/admin-console/models/domain/organization.ts
@@ -31,6 +31,7 @@ export class Organization {
useCustomPermissions: boolean;
useResetPassword: boolean;
useSecretsManager: boolean;
+ usePasswordManager: boolean;
useActivateAutofillPolicy: boolean;
selfHost: boolean;
usersGetPremium: boolean;
@@ -87,6 +88,7 @@ export class Organization {
this.useCustomPermissions = obj.useCustomPermissions;
this.useResetPassword = obj.useResetPassword;
this.useSecretsManager = obj.useSecretsManager;
+ this.usePasswordManager = obj.usePasswordManager;
this.useActivateAutofillPolicy = obj.useActivateAutofillPolicy;
this.selfHost = obj.selfHost;
this.usersGetPremium = obj.usersGetPremium;
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 616c37c00ca..729cf453653 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 bf0eb5f47f8..eba897f31b6 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 a213b07bba7..00000000000
--- 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 2c056ee2875..b248c6d0df9 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;
@@ -27,6 +28,11 @@ export class OrganizationResponse extends BaseResponse {
useResetPassword: boolean;
useSecretsManager: boolean;
hasPublicAndPrivateKeys: boolean;
+ usePasswordManager: boolean;
+ smSeats?: number;
+ smServiceAccounts?: number;
+ maxAutoscaleSmSeats?: number;
+ maxAutoscaleSmServiceAccounts?: number;
constructor(response: any) {
super(response);
@@ -39,8 +45,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");
@@ -55,5 +67,10 @@ export class OrganizationResponse extends BaseResponse {
this.useResetPassword = this.getResponseProperty("UseResetPassword");
this.useSecretsManager = this.getResponseProperty("UseSecretsManager");
this.hasPublicAndPrivateKeys = this.getResponseProperty("HasPublicAndPrivateKeys");
+ this.usePasswordManager = this.getResponseProperty("UsePasswordManager");
+ this.smSeats = this.getResponseProperty("SmSeats");
+ this.smServiceAccounts = this.getResponseProperty("SmServiceAccounts");
+ this.maxAutoscaleSmSeats = this.getResponseProperty("MaxAutoscaleSmSeats");
+ this.maxAutoscaleSmServiceAccounts = this.getResponseProperty("MaxAutoscaleSmServiceAccounts");
}
}
diff --git a/libs/common/src/admin-console/models/response/profile-organization.response.ts b/libs/common/src/admin-console/models/response/profile-organization.response.ts
index 18bf4d45e8a..e042bf145f8 100644
--- a/libs/common/src/admin-console/models/response/profile-organization.response.ts
+++ b/libs/common/src/admin-console/models/response/profile-organization.response.ts
@@ -19,6 +19,7 @@ export class ProfileOrganizationResponse extends BaseResponse {
useCustomPermissions: boolean;
useResetPassword: boolean;
useSecretsManager: boolean;
+ usePasswordManager: boolean;
useActivateAutofillPolicy: boolean;
selfHost: boolean;
usersGetPremium: boolean;
@@ -65,6 +66,7 @@ export class ProfileOrganizationResponse extends BaseResponse {
this.useCustomPermissions = this.getResponseProperty("UseCustomPermissions") ?? false;
this.useResetPassword = this.getResponseProperty("UseResetPassword");
this.useSecretsManager = this.getResponseProperty("UseSecretsManager");
+ this.usePasswordManager = this.getResponseProperty("UsePasswordManager");
this.useActivateAutofillPolicy = this.getResponseProperty("UseActivateAutofillPolicy");
this.selfHost = this.getResponseProperty("SelfHost");
this.usersGetPremium = this.getResponseProperty("UsersGetPremium");
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 3a1d3555242..c235b4b8eea 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
@@ -4,9 +4,11 @@ import { OrganizationSsoRequest } from "../../../auth/models/request/organizatio
import { SecretVerificationRequest } from "../../../auth/models/request/secret-verification.request";
import { ApiKeyResponse } from "../../../auth/models/response/api-key.response";
import { OrganizationSsoResponse } from "../../../auth/models/response/organization-sso.response";
+import { OrganizationSmSubscriptionUpdateRequest } from "../../../billing/models/request/organization-sm-subscription-update.request";
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 +21,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 +121,7 @@ export class OrganizationApiService implements OrganizationApiServiceAbstraction
return new PaymentResponse(r);
}
- async updateSubscription(
+ async updatePasswordManagerSeats(
id: string,
request: OrganizationSubscriptionUpdateRequest
): Promise {
@@ -133,6 +134,19 @@ export class OrganizationApiService implements OrganizationApiServiceAbstraction
);
}
+ async updateSecretsManagerSubscription(
+ id: string,
+ request: OrganizationSmSubscriptionUpdateRequest
+ ): Promise {
+ return this.apiService.send(
+ "POST",
+ "/organizations/" + id + "/sm-subscription",
+ request,
+ true,
+ false
+ );
+ }
+
async updateSeats(id: string, request: SeatRequest): Promise {
const r = await this.apiService.send(
"POST",
@@ -294,13 +308,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.enum.ts b/libs/common/src/billing/enums/bitwarden-product-type.enum.ts
new file mode 100644
index 00000000000..76b0899fd9c
--- /dev/null
+++ b/libs/common/src/billing/enums/bitwarden-product-type.enum.ts
@@ -0,0 +1,4 @@
+export enum BitwardenProductType {
+ PasswordManager = 0,
+ SecretsManager = 1,
+}
diff --git a/libs/common/src/billing/enums/index.ts b/libs/common/src/billing/enums/index.ts
index b4f96cd8fd6..70a3495a8bf 100644
--- a/libs/common/src/billing/enums/index.ts
+++ b/libs/common/src/billing/enums/index.ts
@@ -2,3 +2,4 @@ export * from "./payment-method-type.enum";
export * from "./plan-sponsorship-type.enum";
export * from "./plan-type.enum";
export * from "./transaction-type.enum";
+export * from "./bitwarden-product-type.enum";
diff --git a/libs/common/src/billing/models/request/organization-sm-subscription-update.request.ts b/libs/common/src/billing/models/request/organization-sm-subscription-update.request.ts
new file mode 100644
index 00000000000..7971b1f6a91
--- /dev/null
+++ b/libs/common/src/billing/models/request/organization-sm-subscription-update.request.ts
@@ -0,0 +1,21 @@
+export class OrganizationSmSubscriptionUpdateRequest {
+ /**
+ * The number of seats to add or remove from the subscription.
+ */
+ seatAdjustment: number;
+
+ /**
+ * The maximum number of seats that can be auto-scaled for the subscription.
+ */
+ maxAutoscaleSeats?: number;
+
+ /**
+ * The number of additional service accounts to add or remove from the subscription.
+ */
+ serviceAccountAdjustment: number;
+
+ /**
+ * The maximum number of additional service accounts that can be auto-scaled for the subscription.
+ */
+ maxAutoscaleServiceAccounts?: number;
+}
diff --git a/libs/common/src/billing/models/request/organization-subscription-update.request.ts b/libs/common/src/billing/models/request/organization-subscription-update.request.ts
index 9db3d4be80e..d7566806f6b 100644
--- a/libs/common/src/billing/models/request/organization-subscription-update.request.ts
+++ b/libs/common/src/billing/models/request/organization-subscription-update.request.ts
@@ -1,3 +1,23 @@
export class OrganizationSubscriptionUpdateRequest {
- constructor(public seatAdjustment: number, public maxAutoscaleSeats?: number) {}
+ /**
+ * The number of seats to add or remove from the subscription.
+ * Applies to both PM and SM request types.
+ */
+ seatAdjustment: number;
+
+ /**
+ * The maximum number of seats that can be auto-scaled for the subscription.
+ * Applies to both PM and SM request types.
+ */
+ maxAutoscaleSeats?: number;
+
+ /**
+ * Build a subscription update request for the Password Manager product type.
+ * @param seatAdjustment - The number of seats to add or remove from the subscription.
+ * @param maxAutoscaleSeats - The maximum number of seats that can be auto-scaled for the subscription.
+ */
+ constructor(seatAdjustment: number, maxAutoscaleSeats?: number) {
+ this.seatAdjustment = seatAdjustment;
+ this.maxAutoscaleSeats = maxAutoscaleSeats;
+ }
}
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 00000000000..581d3007c81
--- /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 8368849b86d..bb09a9b143c 100644
--- a/libs/common/src/billing/models/response/plan.response.ts
+++ b/libs/common/src/billing/models/response/plan.response.ts
@@ -1,10 +1,11 @@
import { ProductType } from "../../../enums";
import { BaseResponse } from "../../../models/response/base.response";
-import { PlanType } from "../../enums";
+import { BitwardenProductType, PlanType } from "../../enums";
export class PlanResponse extends BaseResponse {
type: PlanType;
product: ProductType;
+ bitwardenProduct: BitwardenProductType;
name: string;
isAnnual: boolean;
nameLocalizationKey: string;
@@ -48,6 +49,15 @@ export class PlanResponse extends BaseResponse {
additionalStoragePricePerGb: number;
premiumAccessOptionPrice: number;
+ // SM only
+ additionalPricePerServiceAccount: number;
+ baseServiceAccount: number;
+ maxServiceAccount: number;
+ hasAdditionalServiceAccountOption: boolean;
+ maxProjects: number;
+ maxAdditionalServiceAccounts: number;
+ stripeServiceAccountPlanId: string;
+
constructor(response: any) {
super(response);
this.type = this.getResponseProperty("Type");
@@ -90,5 +100,18 @@ 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");
+ this.maxAdditionalServiceAccounts = this.getResponseProperty("MaxAdditionalServiceAccounts");
+ this.stripeServiceAccountPlanId = this.getResponseProperty("StripeServiceAccountPlanId");
}
}
diff --git a/libs/common/src/billing/models/response/subscription.response.ts b/libs/common/src/billing/models/response/subscription.response.ts
index 8230d98417a..ffcc9c23768 100644
--- a/libs/common/src/billing/models/response/subscription.response.ts
+++ b/libs/common/src/billing/models/response/subscription.response.ts
@@ -1,4 +1,5 @@
import { BaseResponse } from "../../../models/response/base.response";
+import { BitwardenProductType } from "../../enums";
export class SubscriptionResponse extends BaseResponse {
storageName: string;
@@ -62,6 +63,8 @@ export class BillingSubscriptionItemResponse extends BaseResponse {
quantity: number;
interval: string;
sponsoredSubscriptionItem: boolean;
+ addonSubscriptionItem: boolean;
+ bitwardenProduct: BitwardenProductType;
constructor(response: any) {
super(response);
@@ -70,6 +73,8 @@ export class BillingSubscriptionItemResponse extends BaseResponse {
this.quantity = this.getResponseProperty("Quantity");
this.interval = this.getResponseProperty("Interval");
this.sponsoredSubscriptionItem = this.getResponseProperty("SponsoredSubscriptionItem");
+ this.addonSubscriptionItem = this.getResponseProperty("AddonSubscriptionItem");
+ this.bitwardenProduct = this.getResponseProperty("BitwardenProduct");
}
}
diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts
index e8a05911b9f..fb155f54e2d 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 ad7c1348896..9c098632b18 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);
}