mirror of
https://github.com/bitwarden/browser.git
synced 2024-12-02 13:23:29 +01:00
[AC-1420] Add Secrets Manager subscribe component (#5617)
This commit is contained in:
parent
797ca073b8
commit
86ccff78cb
@ -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({
|
||||
|
@ -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
|
||||
);
|
||||
|
@ -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 {}
|
||||
|
@ -21,7 +21,7 @@
|
||||
[providerName]="userOrg.providerName"
|
||||
></app-org-subscription-hidden>
|
||||
|
||||
<ng-container *ngIf="sub">
|
||||
<ng-container *ngIf="sub && firstLoaded">
|
||||
<bit-callout
|
||||
type="warning"
|
||||
title="{{ 'canceled' | i18n }}"
|
||||
@ -115,12 +115,12 @@
|
||||
></app-change-plan>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="userOrg.canEditSubscription">
|
||||
<sm-enroll
|
||||
*ngIf="isAdmin"
|
||||
[enabled]="sub?.useSecretsManager"
|
||||
[organizationId]="organizationId"
|
||||
></sm-enroll>
|
||||
<ng-container *ngIf="showSecretsManagerSubscribe">
|
||||
<sm-subscribe-standalone
|
||||
[plan]="sub.secretsManagerPlan"
|
||||
[organization]="userOrg"
|
||||
(onSubscribe)="subscriptionAdjusted()"
|
||||
></sm-subscribe-standalone>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="userOrg.canEditSubscription">
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -1,13 +0,0 @@
|
||||
<form *ngIf="showSecretsManager" [formGroup]="formGroup" [bitSubmit]="submit">
|
||||
<h2 bitTypography="h2" class="tw-mt-7">{{ "secretsManagerBeta" | i18n }}</h2>
|
||||
<p bitTypography="body1">{{ "secretsManagerSubscriptionDesc" | i18n }}</p>
|
||||
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox formControlName="enabled" />
|
||||
<bit-label>{{ "secretsManagerEnable" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
|
||||
<button bitButton bitFormButton buttonType="primary" type="submit">
|
||||
{{ "save" | i18n }}
|
||||
</button>
|
||||
</form>
|
@ -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"));
|
||||
};
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
export * from "./sm-billing.module";
|
||||
export * from "./sm-subscribe.component";
|
||||
export * from "./sm-subscribe-standalone.component";
|
@ -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 {}
|
@ -0,0 +1,8 @@
|
||||
<form [formGroup]="formGroup" [bitSubmit]="submit" *ngIf="plan">
|
||||
<sm-subscribe
|
||||
[formGroup]="formGroup"
|
||||
[selectedPlan]="plan"
|
||||
[upgradeOrganization]="false"
|
||||
[showSubmitButton]="true"
|
||||
></sm-subscribe>
|
||||
</form>
|
@ -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<void>();
|
||||
|
||||
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();
|
||||
};
|
||||
}
|
@ -0,0 +1,68 @@
|
||||
<div *ngIf="formGroup && selectedPlan != null" [formGroup]="formGroup">
|
||||
<h3 bitTypography="h3">{{ "moreFromBitwarden" | i18n }}</h3>
|
||||
<div class="tw-rounded-t tw-bg-background-alt3 tw-p-5">
|
||||
<div class="tw-w-72">
|
||||
<bit-icon [icon]="logo"></bit-icon>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="tw-rounded-b tw-border-x tw-border-b tw-border-t-0 tw-border-solid tw-border-secondary-300 tw-p-5"
|
||||
>
|
||||
<h4 bitTypography="h4">{{ "secretsManagerForPlan" | i18n : planName }}</h4>
|
||||
<div class="tw-text-muted">
|
||||
{{ "secretsManagerForPlanDesc" | i18n }}
|
||||
<ul>
|
||||
<li *ngIf="product == productTypes.Free">{{ "limitedUsers" | i18n : maxUsers }}</li>
|
||||
<li>{{ "unlimitedSecrets" | i18n }}</li>
|
||||
<li *ngIf="product == productTypes.Free; else unlimitedProjects">
|
||||
{{ "projectsIncluded" | i18n : maxProjects }}
|
||||
</li>
|
||||
<ng-template #unlimitedProjects>
|
||||
<li>{{ "unlimitedProjects" | i18n }}</li>
|
||||
</ng-template>
|
||||
<li>{{ "serviceAccountsIncluded" | i18n : serviceAccountsIncluded }}</li>
|
||||
<li *ngIf="product != productTypes.Free">
|
||||
{{
|
||||
"additionalServiceAccountCost" | i18n : (monthlyCostPerServiceAccount | currency : "$")
|
||||
}}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="tw-mb-5">
|
||||
<span *ngIf="product != productTypes.Free; else freeForever">
|
||||
{{ "costPerUser" | i18n : (monthlyCostPerUser | currency : "$") }} /{{ "month" | i18n }}
|
||||
</span>
|
||||
<ng-template #freeForever>
|
||||
<span>{{ "freeForever" | i18n }}</span>
|
||||
</ng-template>
|
||||
</div>
|
||||
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox formControlName="enabled" />
|
||||
<bit-label>{{ "addSecretsManager" | i18n }}</bit-label>
|
||||
<bit-hint *ngIf="upgradeOrganization">{{ "addSecretsManagerUpgradeDesc" | i18n }}</bit-hint>
|
||||
</bit-form-control>
|
||||
|
||||
<div *ngIf="formGroup.value.enabled && selectedPlan.hasAdditionalSeatsOption" class="tw-w-1/2">
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "userSeats" | i18n }}</bit-label>
|
||||
<input bitInput formControlName="userSeats" type="number" />
|
||||
<bit-hint>{{ "userSeatsHowManyDesc" | i18n }}</bit-hint>
|
||||
</bit-form-field>
|
||||
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "additionalServiceAccounts" | i18n }}</bit-label>
|
||||
<input bitInput formControlName="additionalServiceAccounts" type="number" />
|
||||
<bit-hint>{{
|
||||
"additionalServiceAccountsDesc"
|
||||
| i18n : serviceAccountsIncluded : (monthlyCostPerServiceAccount | currency : "$")
|
||||
}}</bit-hint>
|
||||
</bit-form-field>
|
||||
|
||||
<button *ngIf="showSubmitButton" type="submit" bitButton buttonType="primary" bitFormButton>
|
||||
{{ "save" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -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<ControlsOf<SecretsManagerSubscription>> =>
|
||||
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<ControlsOf<SecretsManagerSubscription>>;
|
||||
@Input() upgradeOrganization: boolean;
|
||||
@Input() showSubmitButton = false;
|
||||
@Input() selectedPlan: PlanResponse;
|
||||
|
||||
logo = SecretsManagerLogo;
|
||||
productTypes = ProductType;
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
@ -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"
|
||||
>
|
||||
<app-org-info
|
||||
@ -119,7 +119,7 @@
|
||||
<span *ngIf="selectableProduct.product == productTypes.Free">{{ "freeForever" | i18n }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div *ngIf="formGroup.controls['product'].value !== productTypes.Free">
|
||||
<div *ngIf="formGroup.value.product !== productTypes.Free">
|
||||
<ng-container *ngIf="selectedPlan.hasAdditionalSeatsOption && !selectedPlan.baseSeats">
|
||||
<h2 class="mt-5">{{ "users" | i18n }}</h2>
|
||||
<div class="row">
|
||||
@ -230,7 +230,8 @@
|
||||
<span *ngIf="!selectablePlan.baseSeats">{{ "users" | i18n }}:</span>
|
||||
{{ 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
|
||||
}}
|
||||
</small>
|
||||
@ -256,7 +257,9 @@
|
||||
<span *ngIf="!selectablePlan.baseSeats">{{ "users" | i18n }}:</span>
|
||||
{{ formGroup.controls["additionalSeats"].value || 0 }} ×
|
||||
{{ selectablePlan.seatPrice | currency : "$" }} {{ "monthAbbr" | i18n }} =
|
||||
{{ seatTotal(selectablePlan) | currency : "$" }} /{{ "month" | i18n }}
|
||||
{{ seatTotal(selectablePlan, formGroup.value.additionalSeats) | currency : "$" }} /{{
|
||||
"month" | i18n
|
||||
}}
|
||||
</small>
|
||||
<small *ngIf="selectablePlan.hasAdditionalStorageOption">
|
||||
{{ "additionalStorageGb" | i18n }}:
|
||||
@ -268,8 +271,21 @@
|
||||
</ng-container>
|
||||
</label>
|
||||
</div>
|
||||
<hr class="my-3" />
|
||||
<h2 class="spaced-header mb-4">
|
||||
</div>
|
||||
|
||||
<!-- Secrets Manager -->
|
||||
<div class="tw-my-10">
|
||||
<sm-subscribe
|
||||
*ngIf="showSecretsManagerSubscribe && planOffersSecretsManager"
|
||||
[formGroup]="formGroup.controls.secretsManager"
|
||||
[selectedPlan]="selectedSecretsManagerPlan"
|
||||
[upgradeOrganization]="!createOrganization"
|
||||
></sm-subscribe>
|
||||
</div>
|
||||
|
||||
<!-- Payment info -->
|
||||
<div *ngIf="formGroup.value.product !== productTypes.Free">
|
||||
<h2 class="mb-4">
|
||||
{{ (createOrganization ? "paymentInformation" : "billingInformation") | i18n }}
|
||||
</h2>
|
||||
<small class="text-muted font-italic mb-3 d-block">
|
||||
@ -279,8 +295,12 @@
|
||||
<app-tax-info (onCountryChanged)="changedCountry()"></app-tax-info>
|
||||
<div id="price" class="my-4">
|
||||
<div class="text-muted text-sm">
|
||||
{{ "planPrice" | i18n }}: {{ subtotal | currency : "USD $" }}
|
||||
{{ "passwordManagerPlanPrice" | i18n }}: {{ passwordManagerSubtotal | currency : "USD $" }}
|
||||
<br />
|
||||
<span *ngIf="planOffersSecretsManager && formGroup.value.secretsManager.enabled">
|
||||
{{ "secretsManagerPlanPrice" | i18n }}: {{ secretsManagerSubtotal | currency : "USD $" }}
|
||||
<br />
|
||||
</span>
|
||||
<ng-container>
|
||||
{{ "estimatedTax" | i18n }}: {{ taxCharges | currency : "USD $" }}
|
||||
</ng-container>
|
||||
|
@ -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<void>();
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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<OrganizationResponse>;
|
||||
updatePayment: (id: string, request: PaymentRequest) => Promise<void>;
|
||||
upgrade: (id: string, request: OrganizationUpgradeRequest) => Promise<PaymentResponse>;
|
||||
updateSubscription: (id: string, request: OrganizationSubscriptionUpdateRequest) => Promise<void>;
|
||||
updatePasswordManagerSeats: (
|
||||
id: string,
|
||||
request: OrganizationSubscriptionUpdateRequest
|
||||
) => Promise<void>;
|
||||
updateSeats: (id: string, request: SeatRequest) => Promise<PaymentResponse>;
|
||||
updateStorage: (id: string, request: StorageRequest) => Promise<PaymentResponse>;
|
||||
verifyBank: (id: string, request: VerifyBankRequest) => Promise<void>;
|
||||
@ -60,8 +63,5 @@ export class OrganizationApiServiceAbstraction {
|
||||
getSso: (id: string) => Promise<OrganizationSsoResponse>;
|
||||
updateSso: (id: string, request: OrganizationSsoRequest) => Promise<OrganizationSsoResponse>;
|
||||
selfHostedSyncLicense: (id: string) => Promise<void>;
|
||||
updateEnrollSecretsManager: (
|
||||
id: string,
|
||||
request: OrganizationEnrollSecretsManagerRequest
|
||||
) => Promise<void>;
|
||||
subscribeToSecretsManager: (id: string, request: SecretsManagerSubscribeRequest) => Promise<void>;
|
||||
}
|
||||
|
@ -23,4 +23,8 @@ export class OrganizationCreateRequest {
|
||||
billingAddressState: string;
|
||||
billingAddressPostalCode: string;
|
||||
billingAddressCountry: string;
|
||||
|
||||
useSecretsManager: boolean;
|
||||
additionalSmSeats: number;
|
||||
additionalServiceAccounts: number;
|
||||
}
|
||||
|
@ -11,4 +11,8 @@ export class OrganizationUpgradeRequest {
|
||||
billingAddressCountry: string;
|
||||
billingAddressPostalCode: string;
|
||||
keys: OrganizationKeysRequest;
|
||||
|
||||
useSecretsManager: boolean;
|
||||
additionalSmSeats: number;
|
||||
additionalServiceAccounts: number;
|
||||
}
|
||||
|
@ -1,3 +0,0 @@
|
||||
export class OrganizationEnrollSecretsManagerRequest {
|
||||
enabled: boolean;
|
||||
}
|
@ -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");
|
||||
|
@ -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<void> {
|
||||
@ -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<void> {
|
||||
return await this.apiService.send(
|
||||
"POST",
|
||||
"/organizations/" + id + "/enroll-secrets-manager",
|
||||
"/organizations/" + id + "/subscribe-secrets-manager",
|
||||
request,
|
||||
true,
|
||||
true
|
||||
false
|
||||
);
|
||||
}
|
||||
}
|
||||
|
4
libs/common/src/billing/enums/bitwarden-product-type.ts
Normal file
4
libs/common/src/billing/enums/bitwarden-product-type.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export enum BitwardenProductType {
|
||||
PasswordManager = 0,
|
||||
SecretsManager = 1,
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
export class SecretsManagerSubscribeRequest {
|
||||
additionalSmSeats: number;
|
||||
additionalServiceAccounts: number;
|
||||
}
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
@ -2,4 +2,5 @@ export enum FeatureFlag {
|
||||
DisplayEuEnvironmentFlag = "display-eu-environment",
|
||||
DisplayLowKdfIterationWarningFlag = "display-kdf-iteration-warning",
|
||||
TrustedDeviceEncryption = "trusted-device-encryption",
|
||||
SecretsManagerBilling = "sm-ga-billing",
|
||||
}
|
||||
|
@ -881,7 +881,7 @@ export class ApiService implements ApiServiceAbstraction {
|
||||
// Plan APIs
|
||||
|
||||
async getPlans(): Promise<ListResponse<PlanResponse>> {
|
||||
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);
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user