[AC-1486] Feature: SM Billing Round 1 (#5747)
* [AC-1423] Update organization subscription cloud page (#5614) * [AC-1423] Add ProgressModule to shared.module.ts * [AC-1423] Update cloud subscription page styles - Remove bootstrap styles - Use CL components where applicable - Use CL typography directives - Update heading levels to prepare for new SM sections * [AC-1423] Add usePasswordManager boolean to organization domain * [AC-1423] Introduce BitwardenProductType enum * [AC-1423] Update Organization subscription line items - Add product type prefix - Indent addon services like additional storage and service accounts - Show line items for free plans * [AC-1420] Add Secrets Manager subscribe component (#5617) * [AC-1418] Add secrets manager manage subscription component (#5661) * add additional properties (#5743) * Allow autoscale limits to be removed, update naming (#5781) * [AC-1488] Store Organization.SmServiceAccounts as total not additional (#5784) * Allow autoscale limits to be removed, update naming * Display additional service accounts only * [AC-1531] Fix SM subscribe component not showing in free org billing tab (#5848) --------- Co-authored-by: Shane Melton <smelton@bitwarden.com> Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Co-authored-by: Thomas Rittson <trittson@bitwarden.com> Co-authored-by: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Co-authored-by: Rui Tome <rtome@bitwarden.com>
This commit is contained in:
parent
1a6573ba96
commit
34533f62a9
|
@ -1,18 +1,7 @@
|
||||||
import { Component, EventEmitter, Input, Output } from "@angular/core";
|
import { Component, EventEmitter, Input, Output } from "@angular/core";
|
||||||
import { UntypedFormBuilder, FormGroup } from "@angular/forms";
|
import { FormGroup } from "@angular/forms";
|
||||||
import { Router } from "@angular/router";
|
|
||||||
|
|
||||||
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 { 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";
|
import { OrganizationPlansComponent } from "../../settings/organization-plans.component";
|
||||||
|
|
||||||
|
@ -24,36 +13,6 @@ export class BillingComponent extends OrganizationPlansComponent {
|
||||||
@Input() orgInfoForm: FormGroup;
|
@Input() orgInfoForm: FormGroup;
|
||||||
@Output() previousStep = new EventEmitter();
|
@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() {
|
async ngOnInit() {
|
||||||
const additionalSeats = this.product == ProductType.Families ? 0 : 1;
|
const additionalSeats = this.product == ProductType.Families ? 0 : 1;
|
||||||
this.formGroup.patchValue({
|
this.formGroup.patchValue({
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
|
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
|
||||||
<div>
|
<div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="form-group col-6">
|
<div class="form-group col-8">
|
||||||
<label for="newSeatCount">{{ "subscriptionSeats" | i18n }}</label>
|
<label for="newSeatCount">{{ "subscriptionSeats" | i18n }}</label>
|
||||||
<input
|
<input
|
||||||
id="newSeatCount"
|
id="newSeatCount"
|
||||||
|
@ -41,7 +41,7 @@
|
||||||
<label for="maxAutoscaleSeats">{{ "maxSeatLimit" | i18n }}</label>
|
<label for="maxAutoscaleSeats">{{ "maxSeatLimit" | i18n }}</label>
|
||||||
<input
|
<input
|
||||||
id="maxAutoscaleSeats"
|
id="maxAutoscaleSeats"
|
||||||
class="form-control col-6"
|
class="form-control col-8"
|
||||||
type="number"
|
type="number"
|
||||||
name="MaxAutoscaleSeats"
|
name="MaxAutoscaleSeats"
|
||||||
[(ngModel)]="newMaxSeats"
|
[(ngModel)]="newMaxSeats"
|
||||||
|
|
|
@ -40,7 +40,7 @@ export class AdjustSubscription {
|
||||||
try {
|
try {
|
||||||
const seatAdjustment = this.newSeatCount - this.currentSeatCount;
|
const seatAdjustment = this.newSeatCount - this.currentSeatCount;
|
||||||
const request = new OrganizationSubscriptionUpdateRequest(seatAdjustment, this.newMaxSeats);
|
const request = new OrganizationSubscriptionUpdateRequest(seatAdjustment, this.newMaxSeats);
|
||||||
this.formPromise = this.organizationApiService.updateSubscription(
|
this.formPromise = this.organizationApiService.updatePasswordManagerSeats(
|
||||||
this.organizationId,
|
this.organizationId,
|
||||||
request
|
request
|
||||||
);
|
);
|
||||||
|
|
|
@ -12,7 +12,7 @@ import { OrganizationBillingRoutingModule } from "./organization-billing-routing
|
||||||
import { OrganizationBillingTabComponent } from "./organization-billing-tab.component";
|
import { OrganizationBillingTabComponent } from "./organization-billing-tab.component";
|
||||||
import { OrganizationSubscriptionCloudComponent } from "./organization-subscription-cloud.component";
|
import { OrganizationSubscriptionCloudComponent } from "./organization-subscription-cloud.component";
|
||||||
import { OrganizationSubscriptionSelfhostComponent } from "./organization-subscription-selfhost.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";
|
import { SubscriptionHiddenComponent } from "./subscription-hidden.component";
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
@ -21,6 +21,7 @@ import { SubscriptionHiddenComponent } from "./subscription-hidden.component";
|
||||||
LooseComponentsModule,
|
LooseComponentsModule,
|
||||||
OrganizationBillingRoutingModule,
|
OrganizationBillingRoutingModule,
|
||||||
UserVerificationModule,
|
UserVerificationModule,
|
||||||
|
SecretsManagerBillingModule,
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
AdjustSubscription,
|
AdjustSubscription,
|
||||||
|
@ -32,7 +33,6 @@ import { SubscriptionHiddenComponent } from "./subscription-hidden.component";
|
||||||
OrganizationSubscriptionSelfhostComponent,
|
OrganizationSubscriptionSelfhostComponent,
|
||||||
OrganizationSubscriptionCloudComponent,
|
OrganizationSubscriptionCloudComponent,
|
||||||
SubscriptionHiddenComponent,
|
SubscriptionHiddenComponent,
|
||||||
SecretsManagerEnrollComponent,
|
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class OrganizationBillingModule {}
|
export class OrganizationBillingModule {}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<div class="page-header">
|
<div class="tw-mb-2">
|
||||||
<h1>
|
<h1 bitTypography="h1">
|
||||||
{{ "subscription" | i18n }}
|
{{ "subscription" | i18n }}
|
||||||
<small *ngIf="firstLoaded && loading">
|
<small *ngIf="firstLoaded && loading">
|
||||||
<i
|
<i
|
||||||
|
@ -7,7 +7,7 @@
|
||||||
title="{{ 'loading' | i18n }}"
|
title="{{ 'loading' | i18n }}"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
></i>
|
></i>
|
||||||
<span class="sr-only">{{ "loading" | i18n }}</span>
|
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
|
||||||
</small>
|
</small>
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
@ -21,7 +21,7 @@
|
||||||
[providerName]="userOrg.providerName"
|
[providerName]="userOrg.providerName"
|
||||||
></app-org-subscription-hidden>
|
></app-org-subscription-hidden>
|
||||||
|
|
||||||
<ng-container *ngIf="sub">
|
<ng-container *ngIf="sub && firstLoaded">
|
||||||
<bit-callout
|
<bit-callout
|
||||||
type="warning"
|
type="warning"
|
||||||
title="{{ 'canceled' | i18n }}"
|
title="{{ 'canceled' | i18n }}"
|
||||||
|
@ -40,15 +40,13 @@
|
||||||
</button>
|
</button>
|
||||||
</bit-callout>
|
</bit-callout>
|
||||||
|
|
||||||
<div class="row">
|
<dl class="tw-grid tw-grid-flow-col tw-grid-rows-2">
|
||||||
<div class="col-4">
|
|
||||||
<dl>
|
|
||||||
<dt>{{ "billingPlan" | i18n }}</dt>
|
<dt>{{ "billingPlan" | i18n }}</dt>
|
||||||
<dd>{{ sub.plan.name }}</dd>
|
<dd>{{ sub.plan.name }}</dd>
|
||||||
<ng-container *ngIf="subscription">
|
<ng-container *ngIf="subscription">
|
||||||
<dt>{{ "status" | i18n }}</dt>
|
<dt>{{ "status" | i18n }}</dt>
|
||||||
<dd>
|
<dd>
|
||||||
<span class="text-capitalize">{{
|
<span class="tw-capitalize">{{
|
||||||
isSponsoredSubscription ? "sponsored" : subscription.status || "-"
|
isSponsoredSubscription ? "sponsored" : subscription.status || "-"
|
||||||
}}</span>
|
}}</span>
|
||||||
<span bitBadge badgeType="warning" *ngIf="subscriptionMarkedForCancel">{{
|
<span bitBadge badgeType="warning" *ngIf="subscriptionMarkedForCancel">{{
|
||||||
|
@ -63,25 +61,44 @@
|
||||||
</dd>
|
</dd>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
|
||||||
<ng-container *ngIf="userOrg.canEditSubscription">
|
<ng-container *ngIf="userOrg.canEditSubscription">
|
||||||
<div class="col-8" *ngIf="subscription">
|
<div class="tw-flex-col">
|
||||||
<strong class="d-block mb-1">{{ "details" | i18n }}</strong>
|
<strong class="tw-block tw-border-0 tw-border-b tw-border-solid tw-border-secondary-300">{{
|
||||||
<table class="table">
|
"details" | i18n
|
||||||
<tbody>
|
}}</strong>
|
||||||
<tr *ngFor="let i of subscription.items">
|
<bit-table>
|
||||||
<td>
|
<ng-template body>
|
||||||
|
<ng-container *ngIf="subscription">
|
||||||
|
<tr bitRow *ngFor="let i of lineItems">
|
||||||
|
<td bitCell [ngClass]="{ 'tw-pl-20': i.addonSubscriptionItem }">
|
||||||
|
<span *ngIf="!i.addonSubscriptionItem"
|
||||||
|
>{{ productName(i.bitwardenProduct) }} -</span
|
||||||
|
>
|
||||||
{{ i.name }} {{ i.quantity > 1 ? "×" + i.quantity : "" }} @
|
{{ i.name }} {{ i.quantity > 1 ? "×" + i.quantity : "" }} @
|
||||||
{{ i.amount | currency : "$" }}
|
{{ i.amount | currency : "$" }}
|
||||||
</td>
|
</td>
|
||||||
<td>{{ i.quantity * i.amount | currency : "$" }} /{{ i.interval | i18n }}</td>
|
<td bitCell class="tw-text-right">
|
||||||
|
{{ i.quantity * i.amount | currency : "$" }} /{{ i.interval | i18n }}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</ng-container>
|
||||||
</table>
|
<ng-container *ngIf="userOrg.isFreeOrg">
|
||||||
|
<tr bitRow *ngIf="userOrg.usePasswordManager">
|
||||||
|
<td bitCell>{{ "passwordManager" | i18n }} - {{ "freeOrganization" | i18n }}</td>
|
||||||
|
<td bitCell class="tw-text-right">{{ "free" | i18n }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr bitRow *ngIf="userOrg.useSecretsManager">
|
||||||
|
<td bitCell>{{ "secretsManager" | i18n }} - {{ "freeOrganization" | i18n }}</td>
|
||||||
|
<td bitCell class="tw-text-right">{{ "free" | i18n }}</td>
|
||||||
|
</tr>
|
||||||
|
</ng-container>
|
||||||
|
</ng-template>
|
||||||
|
</bit-table>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</div>
|
|
||||||
<ng-container *ngIf="userOrg.canEditSubscription">
|
<ng-container *ngIf="userOrg.canEditSubscription">
|
||||||
|
<div class="tw-mt-7">
|
||||||
<button
|
<button
|
||||||
bitButton
|
bitButton
|
||||||
buttonType="secondary"
|
buttonType="secondary"
|
||||||
|
@ -97,25 +114,28 @@
|
||||||
(onCanceled)="closeChangePlan()"
|
(onCanceled)="closeChangePlan()"
|
||||||
*ngIf="showChangePlan"
|
*ngIf="showChangePlan"
|
||||||
></app-change-plan>
|
></app-change-plan>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container *ngIf="showSecretsManagerSubscribe">
|
||||||
|
<div class="tw-mt-7">
|
||||||
|
<sm-subscribe-standalone
|
||||||
|
[plan]="sub.secretsManagerPlan"
|
||||||
|
[organization]="userOrg"
|
||||||
|
(onSubscribe)="subscriptionAdjusted()"
|
||||||
|
></sm-subscribe-standalone>
|
||||||
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container *ngIf="userOrg.canEditSubscription">
|
<ng-container *ngIf="userOrg.canEditSubscription">
|
||||||
<sm-enroll
|
<h2 bitTypography="h2" class="tw-mt-7">{{ "manageSubscription" | i18n }}</h2>
|
||||||
*ngIf="isAdmin"
|
<p bitTypography="body1">{{ subscriptionDesc }}</p>
|
||||||
[enabled]="sub?.useSecretsManager"
|
|
||||||
[organizationId]="organizationId"
|
|
||||||
></sm-enroll>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<ng-container *ngIf="userOrg.canEditSubscription">
|
|
||||||
<h2 class="spaced-header">{{ "manageSubscription" | i18n }}</h2>
|
|
||||||
<p class="mb-4">{{ subscriptionDesc }}</p>
|
|
||||||
<ng-container
|
<ng-container
|
||||||
*ngIf="
|
*ngIf="
|
||||||
subscription && canAdjustSeats && !subscription.cancelled && !subscriptionMarkedForCancel
|
subscription && canAdjustSeats && !subscription.cancelled && !subscriptionMarkedForCancel
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<div class="mt-3">
|
<h3 bitTypography="h3" class="tw-mt-7">{{ "passwordManager" | i18n }}</h3>
|
||||||
<app-adjust-subscription
|
<app-adjust-subscription
|
||||||
[seatPrice]="seatPrice"
|
[seatPrice]="seatPrice"
|
||||||
[organizationId]="organizationId"
|
[organizationId]="organizationId"
|
||||||
|
@ -125,7 +145,6 @@
|
||||||
(onAdjusted)="subscriptionAdjusted()"
|
(onAdjusted)="subscriptionAdjusted()"
|
||||||
>
|
>
|
||||||
</app-adjust-subscription>
|
</app-adjust-subscription>
|
||||||
</div>
|
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<button
|
<button
|
||||||
bitButton
|
bitButton
|
||||||
|
@ -136,33 +155,18 @@
|
||||||
>
|
>
|
||||||
{{ "removeSponsorship" | i18n }}
|
{{ "removeSponsorship" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
<h2 class="spaced-header">{{ "storage" | i18n }}</h2>
|
<h4 bitTypography="h4" class="tw-mt-9">{{ "storage" | i18n }}</h4>
|
||||||
<p>{{ "subscriptionStorage" | i18n : sub.maxStorageGb || 0 : sub.storageName || "0 MB" }}</p>
|
<p bitTypography="body1">
|
||||||
<div class="progress">
|
{{ "subscriptionStorage" | i18n : sub.maxStorageGb || 0 : sub.storageName || "0 MB" }}
|
||||||
<div
|
</p>
|
||||||
class="progress-bar bg-success"
|
<bit-progress [barWidth]="storagePercentage" bgColor="success"></bit-progress>
|
||||||
role="progressbar"
|
|
||||||
[ngStyle]="{ width: storageProgressWidth + '%' }"
|
|
||||||
[attr.aria-valuenow]="storagePercentage"
|
|
||||||
aria-valuemin="0"
|
|
||||||
aria-valuemax="100"
|
|
||||||
>
|
|
||||||
{{ storagePercentage / 100 | percent }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<ng-container *ngIf="subscription && !subscription.cancelled && !subscriptionMarkedForCancel">
|
<ng-container *ngIf="subscription && !subscription.cancelled && !subscriptionMarkedForCancel">
|
||||||
<div class="mt-3">
|
<div class="tw-mt-3">
|
||||||
<div class="d-flex" *ngIf="!showAdjustStorage">
|
<div class="tw-flex tw-space-x-2" *ngIf="!showAdjustStorage">
|
||||||
<button bitButton buttonType="secondary" type="button" (click)="adjustStorage(true)">
|
<button bitButton buttonType="secondary" type="button" (click)="adjustStorage(true)">
|
||||||
{{ "addStorage" | i18n }}
|
{{ "addStorage" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button bitButton buttonType="secondary" type="button" (click)="adjustStorage(false)">
|
||||||
bitButton
|
|
||||||
buttonType="secondary"
|
|
||||||
type="button"
|
|
||||||
class="ml-1"
|
|
||||||
(click)="adjustStorage(false)"
|
|
||||||
>
|
|
||||||
{{ "removeStorage" | i18n }}
|
{{ "removeStorage" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -177,13 +181,21 @@
|
||||||
></app-adjust-storage>
|
></app-adjust-storage>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
<ng-container *ngIf="showAdjustSecretsManager">
|
||||||
|
<h3 bitTypography="h3" class="tw-mt-9">{{ "secretsManager" | i18n }}</h3>
|
||||||
|
<app-sm-adjust-subscription
|
||||||
|
[organizationId]="organizationId"
|
||||||
|
[options]="smOptions"
|
||||||
|
(onAdjusted)="subscriptionAdjusted()"
|
||||||
|
></app-sm-adjust-subscription>
|
||||||
|
</ng-container>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<h2 class="spaced-header">{{ "selfHostingTitle" | i18n }}</h2>
|
<h2 bitTypography="h2" class="tw-mt-7">{{ "selfHostingTitle" | i18n }}</h2>
|
||||||
<p class="mb-4">
|
<p bitTypography="body1">
|
||||||
{{ "selfHostingEnterpriseOrganizationSectionCopy" | i18n }}
|
{{ "selfHostingEnterpriseOrganizationSectionCopy" | i18n }}
|
||||||
</p>
|
</p>
|
||||||
<div class="d-flex">
|
<div class="tw-flex tw-space-x-2">
|
||||||
<button
|
<button
|
||||||
bitButton
|
bitButton
|
||||||
buttonType="secondary"
|
buttonType="secondary"
|
||||||
|
@ -198,14 +210,13 @@
|
||||||
bitButton
|
bitButton
|
||||||
buttonType="secondary"
|
buttonType="secondary"
|
||||||
type="button"
|
type="button"
|
||||||
class="ml-1"
|
|
||||||
(click)="manageBillingSync()"
|
(click)="manageBillingSync()"
|
||||||
*ngIf="canManageBillingSync"
|
*ngIf="canManageBillingSync"
|
||||||
>
|
>
|
||||||
{{ (hasBillingSyncToken ? "manageBillingSync" : "setUpBillingSync") | i18n }}
|
{{ (hasBillingSyncToken ? "manageBillingSync" : "setUpBillingSync") | i18n }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-3" *ngIf="showDownloadLicense">
|
<div class="tw-mt-3" *ngIf="showDownloadLicense">
|
||||||
<app-download-license
|
<app-download-license
|
||||||
[organizationId]="organizationId"
|
[organizationId]="organizationId"
|
||||||
(onDownloaded)="closeDownloadLicense()"
|
(onDownloaded)="closeDownloadLicense()"
|
||||||
|
@ -213,17 +224,16 @@
|
||||||
></app-download-license>
|
></app-download-license>
|
||||||
</div>
|
</div>
|
||||||
<ng-container *ngIf="userOrg.canEditSubscription">
|
<ng-container *ngIf="userOrg.canEditSubscription">
|
||||||
<h2 class="spaced-header">{{ "additionalOptions" | i18n }}</h2>
|
<h2 bitTypography="h2" class="tw-mt-7">{{ "additionalOptions" | i18n }}</h2>
|
||||||
<p class="mb-4">
|
<p bitTypography="body1">
|
||||||
{{ "additionalOptionsDesc" | i18n }}
|
{{ "additionalOptionsDesc" | i18n }}
|
||||||
</p>
|
</p>
|
||||||
<div class="d-flex">
|
<div class="tw-flex tw-space-x-2">
|
||||||
<button
|
<button
|
||||||
bitButton
|
bitButton
|
||||||
buttonType="danger"
|
buttonType="danger"
|
||||||
[bitAction]="cancel"
|
[bitAction]="cancel"
|
||||||
type="button"
|
type="button"
|
||||||
class="ml-1"
|
|
||||||
*ngIf="subscription && !subscription.cancelled && !subscriptionMarkedForCancel"
|
*ngIf="subscription && !subscription.cancelled && !subscriptionMarkedForCancel"
|
||||||
>
|
>
|
||||||
{{ "cancelSubscription" | i18n }}
|
{{ "cancelSubscription" | i18n }}
|
||||||
|
|
|
@ -9,8 +9,11 @@ import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-conso
|
||||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||||
import { OrganizationApiKeyType } from "@bitwarden/common/admin-console/enums";
|
import { OrganizationApiKeyType } from "@bitwarden/common/admin-console/enums";
|
||||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||||
import { PlanType } from "@bitwarden/common/billing/enums";
|
import { BitwardenProductType, PlanType } from "@bitwarden/common/billing/enums";
|
||||||
import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.response";
|
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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
|
@ -19,6 +22,7 @@ import {
|
||||||
BillingSyncApiKeyComponent,
|
BillingSyncApiKeyComponent,
|
||||||
BillingSyncApiModalData,
|
BillingSyncApiModalData,
|
||||||
} from "./billing-sync-api-key.component";
|
} from "./billing-sync-api-key.component";
|
||||||
|
import { SecretsManagerSubscriptionOptions } from "./secrets-manager/sm-adjust-subscription.component";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-org-subscription-cloud",
|
selector: "app-org-subscription-cloud",
|
||||||
|
@ -26,6 +30,7 @@ import {
|
||||||
})
|
})
|
||||||
export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy {
|
export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy {
|
||||||
sub: OrganizationSubscriptionResponse;
|
sub: OrganizationSubscriptionResponse;
|
||||||
|
lineItems: BillingSubscriptionItemResponse[] = [];
|
||||||
organizationId: string;
|
organizationId: string;
|
||||||
userOrg: Organization;
|
userOrg: Organization;
|
||||||
showChangePlan = false;
|
showChangePlan = false;
|
||||||
|
@ -33,6 +38,9 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
|
||||||
adjustStorageAdd = true;
|
adjustStorageAdd = true;
|
||||||
showAdjustStorage = false;
|
showAdjustStorage = false;
|
||||||
hasBillingSyncToken: boolean;
|
hasBillingSyncToken: boolean;
|
||||||
|
showAdjustSecretsManager = false;
|
||||||
|
|
||||||
|
showSecretsManagerSubscribe = false;
|
||||||
|
|
||||||
firstLoaded = false;
|
firstLoaded = false;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
|
@ -48,7 +56,8 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
|
||||||
private organizationService: OrganizationService,
|
private organizationService: OrganizationService,
|
||||||
private organizationApiService: OrganizationApiServiceAbstraction,
|
private organizationApiService: OrganizationApiServiceAbstraction,
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
private dialogService: DialogServiceAbstraction
|
private dialogService: DialogServiceAbstraction,
|
||||||
|
private configService: ConfigServiceAbstraction
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
|
@ -68,6 +77,17 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
|
||||||
.subscribe();
|
.subscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
productName(product: BitwardenProductType) {
|
||||||
|
switch (product) {
|
||||||
|
case BitwardenProductType.PasswordManager:
|
||||||
|
return this.i18nService.t("passwordManager");
|
||||||
|
case BitwardenProductType.SecretsManager:
|
||||||
|
return this.i18nService.t("secretsManager");
|
||||||
|
default:
|
||||||
|
return this.i18nService.t("passwordManager");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ngOnDestroy() {
|
ngOnDestroy() {
|
||||||
this.destroy$.next();
|
this.destroy$.next();
|
||||||
this.destroy$.complete();
|
this.destroy$.complete();
|
||||||
|
@ -81,6 +101,7 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
|
||||||
this.userOrg = this.organizationService.get(this.organizationId);
|
this.userOrg = this.organizationService.get(this.organizationId);
|
||||||
if (this.userOrg.canViewSubscription) {
|
if (this.userOrg.canViewSubscription) {
|
||||||
this.sub = await this.organizationApiService.getSubscription(this.organizationId);
|
this.sub = await this.organizationApiService.getSubscription(this.organizationId);
|
||||||
|
this.lineItems = this.sub?.subscription?.items?.sort(sortSubscriptionItems) ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const apiKeyResponse = await this.organizationApiService.getApiKeyInformation(
|
const apiKeyResponse = await this.organizationApiService.getApiKeyInformation(
|
||||||
|
@ -90,7 +111,28 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
|
||||||
(i) => i.keyType === OrganizationApiKeyType.BillingSync
|
(i) => i.keyType === OrganizationApiKeyType.BillingSync
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.showSecretsManagerSubscribe =
|
||||||
|
this.userOrg.canEditSubscription &&
|
||||||
|
!this.userOrg.useSecretsManager &&
|
||||||
|
!this.subscription?.cancelled &&
|
||||||
|
!this.subscriptionMarkedForCancel;
|
||||||
|
|
||||||
|
this.showAdjustSecretsManager =
|
||||||
|
this.userOrg.canEditSubscription &&
|
||||||
|
this.userOrg.useSecretsManager &&
|
||||||
|
this.subscription != null &&
|
||||||
|
this.sub.secretsManagerPlan?.hasAdditionalSeatsOption &&
|
||||||
|
!this.subscription.cancelled &&
|
||||||
|
!this.subscriptionMarkedForCancel;
|
||||||
|
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
|
|
||||||
|
// Remove the remaining lines when the sm-ga-billing flag is deleted
|
||||||
|
const smBillingEnabled = await this.configService.getFeatureFlagBool(
|
||||||
|
FeatureFlag.SecretsManagerBilling
|
||||||
|
);
|
||||||
|
this.showSecretsManagerSubscribe = this.showSecretsManagerSubscribe && smBillingEnabled;
|
||||||
|
this.showAdjustSecretsManager = this.showAdjustSecretsManager && smBillingEnabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
get subscription() {
|
get subscription() {
|
||||||
|
@ -138,6 +180,20 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
|
||||||
return this.sub.seats;
|
return this.sub.seats;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get smOptions(): SecretsManagerSubscriptionOptions {
|
||||||
|
return {
|
||||||
|
seatCount: this.sub.smSeats,
|
||||||
|
maxAutoscaleSeats: this.sub.maxAutoscaleSmSeats,
|
||||||
|
seatPrice: this.sub.secretsManagerPlan.seatPrice,
|
||||||
|
maxAutoscaleServiceAccounts: this.sub.maxAutoscaleSmServiceAccounts,
|
||||||
|
additionalServiceAccounts:
|
||||||
|
this.sub.smServiceAccounts - this.sub.secretsManagerPlan.baseServiceAccount,
|
||||||
|
interval: this.sub.secretsManagerPlan.isAnnual ? "year" : "month",
|
||||||
|
additionalServiceAccountPrice: this.sub.secretsManagerPlan.additionalPricePerServiceAccount,
|
||||||
|
baseServiceAccountCount: this.sub.secretsManagerPlan.baseServiceAccount,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
get maxAutoscaleSeats() {
|
get maxAutoscaleSeats() {
|
||||||
return this.sub.maxAutoscaleSeats;
|
return this.sub.maxAutoscaleSeats;
|
||||||
}
|
}
|
||||||
|
@ -332,3 +388,23 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
|
||||||
return this.subscription == null && this.sub.planType === PlanType.Free && !this.showChangePlan;
|
return this.subscription == null && this.sub.planType === PlanType.Free && !this.showChangePlan;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to sort subscription items by product type and then by addon status
|
||||||
|
*/
|
||||||
|
function sortSubscriptionItems(
|
||||||
|
a: BillingSubscriptionItemResponse,
|
||||||
|
b: BillingSubscriptionItemResponse
|
||||||
|
) {
|
||||||
|
if (a.bitwardenProduct == b.bitwardenProduct) {
|
||||||
|
if (a.addonSubscriptionItem == b.addonSubscriptionItem) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
// sort addon items to the bottom
|
||||||
|
if (a.addonSubscriptionItem) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return a.bitwardenProduct - b.bitwardenProduct;
|
||||||
|
}
|
||||||
|
|
|
@ -1,13 +0,0 @@
|
||||||
<form *ngIf="showSecretsManager" [formGroup]="formGroup" [bitSubmit]="submit">
|
|
||||||
<h2 class="spaced-header">{{ "secretsManagerBeta" | i18n }}</h2>
|
|
||||||
<p>{{ "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,93 @@
|
||||||
|
<form [formGroup]="formGroup" [bitSubmit]="submit">
|
||||||
|
<bit-form-field class="tw-w-2/3">
|
||||||
|
<bit-label>{{ "subscriptionSeats" | i18n }}</bit-label>
|
||||||
|
<input bitInput id="smSeatCount" formControlName="seatCount" type="number" step="1" min="1" />
|
||||||
|
<bit-hint>
|
||||||
|
<strong>{{ "total" | i18n }}:</strong>
|
||||||
|
{{ formGroup.value.seatCount || 0 }} × {{ options.seatPrice | currency : "$" }} =
|
||||||
|
{{ seatTotal | currency : "$" }} / {{ options.interval | i18n }}
|
||||||
|
</bit-hint>
|
||||||
|
</bit-form-field>
|
||||||
|
<bit-form-control>
|
||||||
|
<bit-label>{{ "limitSubscription" | i18n }}</bit-label>
|
||||||
|
<input type="checkbox" bitCheckbox id="limitSmSeats" formControlName="limitSeats" />
|
||||||
|
<bit-hint>
|
||||||
|
{{ "limitSmSubscriptionDesc" | i18n }}
|
||||||
|
</bit-hint>
|
||||||
|
</bit-form-control>
|
||||||
|
<bit-form-field class="tw-w-2/3" *ngIf="formGroup.value.limitSeats">
|
||||||
|
<bit-label>{{ "maxSeatLimit" | i18n }}</bit-label>
|
||||||
|
<input
|
||||||
|
bitInput
|
||||||
|
id="smSeatLimit"
|
||||||
|
formControlName="maxAutoscaleSeats"
|
||||||
|
type="number"
|
||||||
|
step="1"
|
||||||
|
[min]="formGroup.value.seatCount"
|
||||||
|
/>
|
||||||
|
<bit-hint>
|
||||||
|
<strong>{{ "maxSeatCost" | i18n }}:</strong>
|
||||||
|
{{ formGroup.value.maxAutoscaleSeats || 0 }} ×
|
||||||
|
{{ options.seatPrice | currency : "$" }} = {{ maxSeatTotal | currency : "$" }} /
|
||||||
|
{{ options.interval | i18n }}
|
||||||
|
</bit-hint>
|
||||||
|
</bit-form-field>
|
||||||
|
<bit-form-field class="tw-w-2/3">
|
||||||
|
<bit-label>{{ "additionalServiceAccounts" | i18n }}</bit-label>
|
||||||
|
<input
|
||||||
|
bitInput
|
||||||
|
id="additionalServiceAccountCount"
|
||||||
|
formControlName="additionalServiceAccounts"
|
||||||
|
type="number"
|
||||||
|
step="1"
|
||||||
|
min="0"
|
||||||
|
/>
|
||||||
|
<bit-hint>
|
||||||
|
<div>
|
||||||
|
{{
|
||||||
|
"additionalServiceAccountsDesc"
|
||||||
|
| i18n : options.baseServiceAccountCount : (monthlyServiceAccountPrice | currency : "$")
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong>{{ "total" | i18n }}:</strong>
|
||||||
|
{{ formGroup.value.additionalServiceAccounts || 0 }} ×
|
||||||
|
{{ options.additionalServiceAccountPrice | currency : "$" }} =
|
||||||
|
{{ serviceAccountTotal | currency : "$" }} / {{ options.interval | i18n }}
|
||||||
|
</div>
|
||||||
|
</bit-hint>
|
||||||
|
</bit-form-field>
|
||||||
|
<bit-form-control>
|
||||||
|
<bit-label>{{ "limitServiceAccounts" | i18n }}</bit-label>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
bitCheckbox
|
||||||
|
id="limitServiceAccounts"
|
||||||
|
formControlName="limitServiceAccounts"
|
||||||
|
/>
|
||||||
|
<bit-hint>
|
||||||
|
{{ "limitServiceAccountsDesc" | i18n }}
|
||||||
|
</bit-hint>
|
||||||
|
</bit-form-control>
|
||||||
|
<bit-form-field class="tw-w-2/3" *ngIf="formGroup.value.limitServiceAccounts">
|
||||||
|
<bit-label>{{ "serviceAccountLimit" | i18n }}</bit-label>
|
||||||
|
<input
|
||||||
|
bitInput
|
||||||
|
id="additionalServiceAccountLimit"
|
||||||
|
formControlName="maxAutoscaleServiceAccounts"
|
||||||
|
type="number"
|
||||||
|
step="1"
|
||||||
|
[min]="formGroup.value.additionalServiceAccounts"
|
||||||
|
/>
|
||||||
|
<bit-hint>
|
||||||
|
<strong>{{ "maxServiceAccountCost" | i18n }}:</strong>
|
||||||
|
{{ formGroup.value.maxAutoscaleServiceAccounts || 0 }} ×
|
||||||
|
{{ options.additionalServiceAccountPrice | currency : "$" }} =
|
||||||
|
{{ maxServiceAccountTotal | currency : "$" }} / {{ options.interval | i18n }}
|
||||||
|
</bit-hint>
|
||||||
|
</bit-form-field>
|
||||||
|
<button type="submit" bitButton buttonType="primary" bitFormButton>
|
||||||
|
{{ "save" | i18n }}
|
||||||
|
</button>
|
||||||
|
<bit-error-summary [formGroup]="formGroup" class="tw-mt-2"></bit-error-summary>
|
||||||
|
</form>
|
|
@ -0,0 +1,172 @@
|
||||||
|
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core";
|
||||||
|
import { FormBuilder, Validators } from "@angular/forms";
|
||||||
|
import { Subject, takeUntil } from "rxjs";
|
||||||
|
|
||||||
|
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||||
|
import { OrganizationSmSubscriptionUpdateRequest } from "@bitwarden/common/billing/models/request/organization-sm-subscription-update.request";
|
||||||
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
|
|
||||||
|
export interface SecretsManagerSubscriptionOptions {
|
||||||
|
interval: "year" | "month";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current number of seats the organization subscribes to.
|
||||||
|
*/
|
||||||
|
seatCount: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional auto-scaling limit for the number of seats the organization can subscribe to.
|
||||||
|
*/
|
||||||
|
maxAutoscaleSeats: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The price per seat for the subscription.
|
||||||
|
*/
|
||||||
|
seatPrice: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The number of service accounts that are included in the base subscription.
|
||||||
|
*/
|
||||||
|
baseServiceAccountCount: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current number of additional service accounts the organization subscribes to.
|
||||||
|
*/
|
||||||
|
additionalServiceAccounts: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional auto-scaling limit for the number of additional service accounts the organization can subscribe to.
|
||||||
|
*/
|
||||||
|
maxAutoscaleServiceAccounts: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The price per additional service account for the subscription.
|
||||||
|
*/
|
||||||
|
additionalServiceAccountPrice: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "app-sm-adjust-subscription",
|
||||||
|
templateUrl: "sm-adjust-subscription.component.html",
|
||||||
|
})
|
||||||
|
export class SecretsManagerAdjustSubscriptionComponent implements OnInit, OnDestroy {
|
||||||
|
@Input() organizationId: string;
|
||||||
|
@Input() options: SecretsManagerSubscriptionOptions;
|
||||||
|
@Output() onAdjusted = new EventEmitter();
|
||||||
|
|
||||||
|
private destroy$ = new Subject<void>();
|
||||||
|
|
||||||
|
formGroup = this.formBuilder.group({
|
||||||
|
seatCount: [0, [Validators.required, Validators.min(1)]],
|
||||||
|
limitSeats: [false],
|
||||||
|
maxAutoscaleSeats: [null as number | null],
|
||||||
|
additionalServiceAccounts: [0, [Validators.required, Validators.min(0)]],
|
||||||
|
limitServiceAccounts: [false],
|
||||||
|
maxAutoscaleServiceAccounts: [null as number | null],
|
||||||
|
});
|
||||||
|
|
||||||
|
get monthlyServiceAccountPrice(): number {
|
||||||
|
return this.options.interval == "month"
|
||||||
|
? this.options.additionalServiceAccountPrice
|
||||||
|
: this.options.additionalServiceAccountPrice / 12;
|
||||||
|
}
|
||||||
|
|
||||||
|
get serviceAccountTotal(): number {
|
||||||
|
return Math.abs(
|
||||||
|
this.formGroup.value.additionalServiceAccounts * this.options.additionalServiceAccountPrice
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
get seatTotal(): number {
|
||||||
|
return Math.abs(this.formGroup.value.seatCount * this.options.seatPrice);
|
||||||
|
}
|
||||||
|
|
||||||
|
get maxServiceAccountTotal(): number {
|
||||||
|
return Math.abs(
|
||||||
|
(this.formGroup.value.maxAutoscaleServiceAccounts ?? 0) *
|
||||||
|
this.options.additionalServiceAccountPrice
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
get maxSeatTotal(): number {
|
||||||
|
return Math.abs((this.formGroup.value.maxAutoscaleSeats ?? 0) * this.options.seatPrice);
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private formBuilder: FormBuilder,
|
||||||
|
private organizationApiService: OrganizationApiServiceAbstraction,
|
||||||
|
private i18nService: I18nService,
|
||||||
|
private platformUtilsService: PlatformUtilsService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.formGroup.valueChanges.pipe(takeUntil(this.destroy$)).subscribe((value) => {
|
||||||
|
const maxAutoscaleSeatsControl = this.formGroup.controls.maxAutoscaleSeats;
|
||||||
|
const maxAutoscaleServiceAccountsControl =
|
||||||
|
this.formGroup.controls.maxAutoscaleServiceAccounts;
|
||||||
|
|
||||||
|
if (value.limitSeats) {
|
||||||
|
maxAutoscaleSeatsControl.setValidators([Validators.min(value.seatCount)]);
|
||||||
|
maxAutoscaleSeatsControl.enable({ emitEvent: false });
|
||||||
|
} else {
|
||||||
|
maxAutoscaleSeatsControl.disable({ emitEvent: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.limitServiceAccounts) {
|
||||||
|
maxAutoscaleServiceAccountsControl.setValidators([
|
||||||
|
Validators.min(value.additionalServiceAccounts),
|
||||||
|
]);
|
||||||
|
maxAutoscaleServiceAccountsControl.enable({ emitEvent: false });
|
||||||
|
} else {
|
||||||
|
maxAutoscaleServiceAccountsControl.disable({ emitEvent: false });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.formGroup.patchValue({
|
||||||
|
seatCount: this.options.seatCount,
|
||||||
|
maxAutoscaleSeats: this.options.maxAutoscaleSeats,
|
||||||
|
additionalServiceAccounts: this.options.additionalServiceAccounts,
|
||||||
|
maxAutoscaleServiceAccounts: this.options.maxAutoscaleServiceAccounts,
|
||||||
|
limitSeats: this.options.maxAutoscaleSeats != null,
|
||||||
|
limitServiceAccounts: this.options.maxAutoscaleServiceAccounts != null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
submit = async () => {
|
||||||
|
this.formGroup.markAllAsTouched();
|
||||||
|
|
||||||
|
if (this.formGroup.invalid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const request = new OrganizationSmSubscriptionUpdateRequest();
|
||||||
|
request.seatAdjustment = this.formGroup.value.seatCount - this.options.seatCount;
|
||||||
|
request.serviceAccountAdjustment =
|
||||||
|
this.formGroup.value.additionalServiceAccounts - this.options.additionalServiceAccounts;
|
||||||
|
request.maxAutoscaleSeats = this.formGroup.value.limitSeats
|
||||||
|
? this.formGroup.value.maxAutoscaleSeats
|
||||||
|
: null;
|
||||||
|
request.maxAutoscaleServiceAccounts = this.formGroup.value.limitServiceAccounts
|
||||||
|
? this.formGroup.value.maxAutoscaleServiceAccounts
|
||||||
|
: null;
|
||||||
|
|
||||||
|
await this.organizationApiService.updateSecretsManagerSubscription(
|
||||||
|
this.organizationId,
|
||||||
|
request
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.platformUtilsService.showToast(
|
||||||
|
"success",
|
||||||
|
null,
|
||||||
|
this.i18nService.t("subscriptionUpdated")
|
||||||
|
);
|
||||||
|
|
||||||
|
this.onAdjusted.emit();
|
||||||
|
};
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
this.destroy$.next();
|
||||||
|
this.destroy$.complete();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { NgModule } from "@angular/core";
|
||||||
|
|
||||||
|
import { SharedModule } from "../../../shared";
|
||||||
|
|
||||||
|
import { SecretsManagerAdjustSubscriptionComponent } from "./sm-adjust-subscription.component";
|
||||||
|
import { SecretsManagerSubscribeStandaloneComponent } from "./sm-subscribe-standalone.component";
|
||||||
|
import { SecretsManagerSubscribeComponent } from "./sm-subscribe.component";
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [SharedModule],
|
||||||
|
declarations: [
|
||||||
|
SecretsManagerSubscribeComponent,
|
||||||
|
SecretsManagerSubscribeStandaloneComponent,
|
||||||
|
SecretsManagerAdjustSubscriptionComponent,
|
||||||
|
],
|
||||||
|
exports: [
|
||||||
|
SecretsManagerSubscribeComponent,
|
||||||
|
SecretsManagerSubscribeStandaloneComponent,
|
||||||
|
SecretsManagerAdjustSubscriptionComponent,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
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,46 @@
|
||||||
|
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.plan.hasAdditionalSeatsOption
|
||||||
|
? this.formGroup.value.userSeats
|
||||||
|
: 0;
|
||||||
|
request.additionalServiceAccounts = this.plan.hasAdditionalServiceAccountOption
|
||||||
|
? this.formGroup.value.additionalServiceAccounts
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
await this.organizationApiService.subscribeToSecretsManager(this.organization.id, request);
|
||||||
|
|
||||||
|
this.platformUtilsService.showToast("success", null, this.i18nService.t("subscriptionUpdated"));
|
||||||
|
|
||||||
|
this.onSubscribe.emit();
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,71 @@
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<ng-container *ngIf="formGroup.value.enabled">
|
||||||
|
<div *ngIf="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>
|
||||||
|
</div>
|
||||||
|
<div *ngIf="selectedPlan.hasAdditionalServiceAccountOption" class="tw-w-1/2">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button *ngIf="showSubmitButton" type="submit" bitButton buttonType="primary" bitFormButton>
|
||||||
|
{{ "save" | i18n }}
|
||||||
|
</button>
|
||||||
|
</ng-container>
|
||||||
|
</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()"
|
(ngSubmit)="submit()"
|
||||||
[appApiAction]="formPromise"
|
[appApiAction]="formPromise"
|
||||||
ngNativeValidate
|
ngNativeValidate
|
||||||
*ngIf="!loading && !selfHosted && this.plans"
|
*ngIf="!loading && !selfHosted && this.passwordManagerPlans && this.secretsManagerPlans"
|
||||||
class="tw-pt-6"
|
class="tw-pt-6"
|
||||||
>
|
>
|
||||||
<app-org-info
|
<app-org-info
|
||||||
|
@ -119,7 +119,7 @@
|
||||||
<span *ngIf="selectableProduct.product == productTypes.Free">{{ "freeForever" | i18n }}</span>
|
<span *ngIf="selectableProduct.product == productTypes.Free">{{ "freeForever" | i18n }}</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="formGroup.controls['product'].value !== productTypes.Free">
|
<div *ngIf="formGroup.value.product !== productTypes.Free">
|
||||||
<ng-container *ngIf="selectedPlan.hasAdditionalSeatsOption && !selectedPlan.baseSeats">
|
<ng-container *ngIf="selectedPlan.hasAdditionalSeatsOption && !selectedPlan.baseSeats">
|
||||||
<h2 class="mt-5">{{ "users" | i18n }}</h2>
|
<h2 class="mt-5">{{ "users" | i18n }}</h2>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
@ -230,7 +230,8 @@
|
||||||
<span *ngIf="!selectablePlan.baseSeats">{{ "users" | i18n }}:</span>
|
<span *ngIf="!selectablePlan.baseSeats">{{ "users" | i18n }}:</span>
|
||||||
{{ formGroup.controls["additionalSeats"].value || 0 }} ×
|
{{ formGroup.controls["additionalSeats"].value || 0 }} ×
|
||||||
{{ selectablePlan.seatPrice / 12 | currency : "$" }} × 12
|
{{ selectablePlan.seatPrice / 12 | currency : "$" }} × 12
|
||||||
{{ "monthAbbr" | i18n }} = {{ seatTotal(selectablePlan) | currency : "$" }} /{{
|
{{ "monthAbbr" | i18n }} =
|
||||||
|
{{ seatTotal(selectablePlan, formGroup.value.additionalSeats) | currency : "$" }} /{{
|
||||||
"year" | i18n
|
"year" | i18n
|
||||||
}}
|
}}
|
||||||
</small>
|
</small>
|
||||||
|
@ -256,7 +257,9 @@
|
||||||
<span *ngIf="!selectablePlan.baseSeats">{{ "users" | i18n }}:</span>
|
<span *ngIf="!selectablePlan.baseSeats">{{ "users" | i18n }}:</span>
|
||||||
{{ formGroup.controls["additionalSeats"].value || 0 }} ×
|
{{ formGroup.controls["additionalSeats"].value || 0 }} ×
|
||||||
{{ selectablePlan.seatPrice | currency : "$" }} {{ "monthAbbr" | i18n }} =
|
{{ selectablePlan.seatPrice | currency : "$" }} {{ "monthAbbr" | i18n }} =
|
||||||
{{ seatTotal(selectablePlan) | currency : "$" }} /{{ "month" | i18n }}
|
{{ seatTotal(selectablePlan, formGroup.value.additionalSeats) | currency : "$" }} /{{
|
||||||
|
"month" | i18n
|
||||||
|
}}
|
||||||
</small>
|
</small>
|
||||||
<small *ngIf="selectablePlan.hasAdditionalStorageOption">
|
<small *ngIf="selectablePlan.hasAdditionalStorageOption">
|
||||||
{{ "additionalStorageGb" | i18n }}:
|
{{ "additionalStorageGb" | i18n }}:
|
||||||
|
@ -268,8 +271,21 @@
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<hr class="my-3" />
|
</div>
|
||||||
<h2 class="spaced-header mb-4">
|
|
||||||
|
<!-- 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 }}
|
{{ (createOrganization ? "paymentInformation" : "billingInformation") | i18n }}
|
||||||
</h2>
|
</h2>
|
||||||
<small class="text-muted font-italic mb-3 d-block">
|
<small class="text-muted font-italic mb-3 d-block">
|
||||||
|
@ -279,8 +295,12 @@
|
||||||
<app-tax-info (onCountryChanged)="changedCountry()"></app-tax-info>
|
<app-tax-info (onCountryChanged)="changedCountry()"></app-tax-info>
|
||||||
<div id="price" class="my-4">
|
<div id="price" class="my-4">
|
||||||
<div class="text-muted text-sm">
|
<div class="text-muted text-sm">
|
||||||
{{ "planPrice" | i18n }}: {{ subtotal | currency : "USD $" }}
|
{{ "passwordManagerPlanPrice" | i18n }}: {{ passwordManagerSubtotal | currency : "USD $" }}
|
||||||
<br />
|
<br />
|
||||||
|
<span *ngIf="planOffersSecretsManager && formGroup.value.secretsManager.enabled">
|
||||||
|
{{ "secretsManagerPlanPrice" | i18n }}: {{ secretsManagerSubtotal | currency : "USD $" }}
|
||||||
|
<br />
|
||||||
|
</span>
|
||||||
<ng-container>
|
<ng-container>
|
||||||
{{ "estimatedTax" | i18n }}: {{ taxCharges | currency : "USD $" }}
|
{{ "estimatedTax" | i18n }}: {{ taxCharges | currency : "USD $" }}
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
|
@ -7,7 +7,7 @@ import {
|
||||||
Output,
|
Output,
|
||||||
ViewChild,
|
ViewChild,
|
||||||
} from "@angular/core";
|
} from "@angular/core";
|
||||||
import { UntypedFormBuilder, Validators } from "@angular/forms";
|
import { FormBuilder, Validators } from "@angular/forms";
|
||||||
import { Router } from "@angular/router";
|
import { Router } from "@angular/router";
|
||||||
import { Subject, takeUntil } from "rxjs";
|
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 { OrganizationKeysRequest } from "@bitwarden/common/admin-console/models/request/organization-keys.request";
|
||||||
import { OrganizationUpgradeRequest } from "@bitwarden/common/admin-console/models/request/organization-upgrade.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 { 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 { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response";
|
||||||
import { ProductType } from "@bitwarden/common/enums";
|
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 { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.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 { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
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 { PaymentComponent } from "./payment.component";
|
||||||
import { TaxInfoComponent } from "./tax-info.component";
|
import { TaxInfoComponent } from "./tax-info.component";
|
||||||
|
|
||||||
|
@ -51,24 +55,29 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
|
||||||
@Input() showFree = true;
|
@Input() showFree = true;
|
||||||
@Input() showCancel = false;
|
@Input() showCancel = false;
|
||||||
@Input() acceptingSponsorship = false;
|
@Input() acceptingSponsorship = false;
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
get product(): ProductType {
|
get product(): ProductType {
|
||||||
return this._product;
|
return this._product;
|
||||||
}
|
}
|
||||||
|
|
||||||
set product(product: ProductType) {
|
set product(product: ProductType) {
|
||||||
this._product = product;
|
this._product = product;
|
||||||
this.formGroup?.controls?.product?.setValue(product);
|
this.formGroup?.controls?.product?.setValue(product);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _product = ProductType.Free;
|
private _product = ProductType.Free;
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
get plan(): PlanType {
|
get plan(): PlanType {
|
||||||
return this._plan;
|
return this._plan;
|
||||||
}
|
}
|
||||||
|
|
||||||
set plan(plan: PlanType) {
|
set plan(plan: PlanType) {
|
||||||
this._plan = plan;
|
this._plan = plan;
|
||||||
this.formGroup?.controls?.plan?.setValue(plan);
|
this.formGroup?.controls?.plan?.setValue(plan);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _plan = PlanType.Free;
|
private _plan = PlanType.Free;
|
||||||
@Input() providerId?: string;
|
@Input() providerId?: string;
|
||||||
@Output() onSuccess = new EventEmitter<OnSuccessArgs>();
|
@Output() onSuccess = new EventEmitter<OnSuccessArgs>();
|
||||||
|
@ -82,6 +91,9 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
|
||||||
singleOrgPolicyAppliesToActiveUser = false;
|
singleOrgPolicyAppliesToActiveUser = false;
|
||||||
isInTrialFlow = false;
|
isInTrialFlow = false;
|
||||||
discount = 0;
|
discount = 0;
|
||||||
|
showSecretsManagerSubscribe: boolean;
|
||||||
|
|
||||||
|
secretsManagerSubscription = secretsManagerSubscribeFormFactory(this.formBuilder);
|
||||||
|
|
||||||
formGroup = this.formBuilder.group({
|
formGroup = this.formBuilder.group({
|
||||||
name: [""],
|
name: [""],
|
||||||
|
@ -94,9 +106,11 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
|
||||||
businessName: [""],
|
businessName: [""],
|
||||||
plan: [this.plan],
|
plan: [this.plan],
|
||||||
product: [this.product],
|
product: [this.product],
|
||||||
|
secretsManager: this.secretsManagerSubscription,
|
||||||
});
|
});
|
||||||
|
|
||||||
plans: PlanResponse[];
|
passwordManagerPlans: PlanResponse[];
|
||||||
|
secretsManagerPlans: PlanResponse[];
|
||||||
|
|
||||||
private destroy$ = new Subject<void>();
|
private destroy$ = new Subject<void>();
|
||||||
|
|
||||||
|
@ -111,8 +125,9 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
|
||||||
private organizationService: OrganizationService,
|
private organizationService: OrganizationService,
|
||||||
private logService: LogService,
|
private logService: LogService,
|
||||||
private messagingService: MessagingService,
|
private messagingService: MessagingService,
|
||||||
private formBuilder: UntypedFormBuilder,
|
private formBuilder: FormBuilder,
|
||||||
private organizationApiService: OrganizationApiServiceAbstraction
|
private organizationApiService: OrganizationApiServiceAbstraction,
|
||||||
|
private configService: ConfigServiceAbstraction
|
||||||
) {
|
) {
|
||||||
this.selfHosted = platformUtilsService.isSelfHost();
|
this.selfHosted = platformUtilsService.isSelfHost();
|
||||||
}
|
}
|
||||||
|
@ -120,7 +135,13 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
if (!this.selfHosted) {
|
if (!this.selfHosted) {
|
||||||
const plans = await this.apiService.getPlans();
|
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) {
|
if (this.product === ProductType.Enterprise || this.product === ProductType.Teams) {
|
||||||
this.formGroup.controls.businessOwned.setValue(true);
|
this.formGroup.controls.businessOwned.setValue(true);
|
||||||
}
|
}
|
||||||
|
@ -131,12 +152,9 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
|
||||||
this.changedOwnedBusiness();
|
this.changedOwnedBusiness();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.createOrganization || this.acceptingSponsorship) {
|
if (!this.createOrganization) {
|
||||||
this.formGroup.controls.product.setValue(ProductType.Families);
|
this.upgradeFlowPrefillForm();
|
||||||
this.changedProduct();
|
} else {
|
||||||
}
|
|
||||||
|
|
||||||
if (this.createOrganization) {
|
|
||||||
this.formGroup.controls.name.addValidators([Validators.required, Validators.maxLength(50)]);
|
this.formGroup.controls.name.addValidators([Validators.required, Validators.maxLength(50)]);
|
||||||
this.formGroup.controls.billingEmail.addValidators(Validators.required);
|
this.formGroup.controls.billingEmail.addValidators(Validators.required);
|
||||||
}
|
}
|
||||||
|
@ -148,6 +166,11 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
|
||||||
this.singleOrgPolicyAppliesToActiveUser = policyAppliesToActiveUser;
|
this.singleOrgPolicyAppliesToActiveUser = policyAppliesToActiveUser;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.showSecretsManagerSubscribe = await this.configService.getFeatureFlagBool(
|
||||||
|
FeatureFlag.SecretsManagerBilling,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -165,7 +188,15 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
get selectedPlan() {
|
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() {
|
get selectedPlanInterval() {
|
||||||
|
@ -173,7 +204,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
get selectableProducts() {
|
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) {
|
if (this.formGroup.controls.businessOwned.value) {
|
||||||
validPlans = validPlans.filter((plan) => plan.canBeUsedByBusiness);
|
validPlans = validPlans.filter((plan) => plan.canBeUsedByBusiness);
|
||||||
|
@ -191,7 +222,9 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (this.acceptingSponsorship) {
|
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;
|
this.discount = familyPlan.basePrice;
|
||||||
validPlans = [familyPlan];
|
validPlans = [familyPlan];
|
||||||
}
|
}
|
||||||
|
@ -200,7 +233,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
get selectablePlans() {
|
get selectablePlans() {
|
||||||
return this.plans?.filter(
|
return this.passwordManagerPlans?.filter(
|
||||||
(plan) =>
|
(plan) =>
|
||||||
!plan.legacyYear && !plan.disabled && plan.product === this.formGroup.controls.product.value
|
!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) {
|
if (!plan.hasAdditionalSeatsOption) {
|
||||||
return 0;
|
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;
|
let subTotal = this.selectedPlan.basePrice;
|
||||||
if (
|
if (
|
||||||
this.selectedPlan.hasAdditionalSeatsOption &&
|
this.selectedPlan.hasAdditionalSeatsOption &&
|
||||||
this.formGroup.controls.additionalSeats.value
|
this.formGroup.controls.additionalSeats.value
|
||||||
) {
|
) {
|
||||||
subTotal += this.seatTotal(this.selectedPlan);
|
subTotal += this.seatTotal(this.selectedPlan, this.formGroup.value.additionalSeats);
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
this.selectedPlan.hasAdditionalStorageOption &&
|
this.selectedPlan.hasAdditionalStorageOption &&
|
||||||
|
@ -262,18 +306,39 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
|
||||||
return subTotal - this.discount;
|
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() {
|
get freeTrial() {
|
||||||
return this.selectedPlan.trialPeriodDays != null;
|
return this.selectedPlan.trialPeriodDays != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
get taxCharges() {
|
get taxCharges() {
|
||||||
return this.taxComponent != null && this.taxComponent.taxRate != null
|
return this.taxComponent != null && this.taxComponent.taxRate != null
|
||||||
? (this.taxComponent.taxRate / 100) * this.subtotal
|
? (this.taxComponent.taxRate / 100) *
|
||||||
|
(this.passwordManagerSubtotal + this.secretsManagerSubtotal)
|
||||||
: 0;
|
: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
get total() {
|
get total() {
|
||||||
return this.subtotal + this.taxCharges || 0;
|
return this.passwordManagerSubtotal + this.secretsManagerSubtotal + this.taxCharges || 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
get paymentDesc() {
|
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() {
|
changedProduct() {
|
||||||
this.formGroup.controls.plan.setValue(this.selectablePlans[0].type);
|
this.formGroup.controls.plan.setValue(this.selectablePlans[0].type);
|
||||||
if (!this.selectedPlan.hasPremiumAccessOption) {
|
if (!this.selectedPlan.hasPremiumAccessOption) {
|
||||||
|
@ -303,6 +376,14 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
|
||||||
) {
|
) {
|
||||||
this.formGroup.controls.additionalSeats.setValue(1);
|
this.formGroup.controls.additionalSeats.setValue(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.planOffersSecretsManager) {
|
||||||
|
this.secretsManagerForm.enable();
|
||||||
|
} else {
|
||||||
|
this.secretsManagerForm.disable();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.secretsManagerForm.updateValueAndValidity();
|
||||||
}
|
}
|
||||||
|
|
||||||
changedOwnedBusiness() {
|
changedOwnedBusiness() {
|
||||||
|
@ -407,6 +488,9 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
|
||||||
request.billingAddressCountry = this.taxComponent.taxInfo.country;
|
request.billingAddressCountry = this.taxComponent.taxInfo.country;
|
||||||
request.billingAddressPostalCode = this.taxComponent.taxInfo.postalCode;
|
request.billingAddressPostalCode = this.taxComponent.taxInfo.postalCode;
|
||||||
|
|
||||||
|
// Secrets Manager
|
||||||
|
this.buildSecretsManagerRequest(request);
|
||||||
|
|
||||||
// Retrieve org info to backfill pub/priv key if necessary
|
// Retrieve org info to backfill pub/priv key if necessary
|
||||||
const org = await this.organizationService.get(this.organizationId);
|
const org = await this.organizationService.get(this.organizationId);
|
||||||
if (!org.hasPublicAndPrivateKeys) {
|
if (!org.hasPublicAndPrivateKeys) {
|
||||||
|
@ -462,6 +546,9 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Secrets Manager
|
||||||
|
this.buildSecretsManagerRequest(request);
|
||||||
|
|
||||||
if (this.providerId) {
|
if (this.providerId) {
|
||||||
const providerRequest = new ProviderOrganizationCreateRequest(
|
const providerRequest = new ProviderOrganizationCreateRequest(
|
||||||
this.formGroup.controls.clientOwnerEmail.value,
|
this.formGroup.controls.clientOwnerEmail.value,
|
||||||
|
@ -517,4 +604,40 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
return text;
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -51,6 +51,7 @@ import { UpdatePasswordComponent } from "../auth/update-password.component";
|
||||||
import { UpdateTempPasswordComponent } from "../auth/update-temp-password.component";
|
import { UpdateTempPasswordComponent } from "../auth/update-temp-password.component";
|
||||||
import { VerifyEmailTokenComponent } from "../auth/verify-email-token.component";
|
import { VerifyEmailTokenComponent } from "../auth/verify-email-token.component";
|
||||||
import { VerifyRecoverDeleteComponent } from "../auth/verify-recover-delete.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 { AddCreditComponent } from "../billing/settings/add-credit.component";
|
||||||
import { AdjustPaymentComponent } from "../billing/settings/adjust-payment.component";
|
import { AdjustPaymentComponent } from "../billing/settings/adjust-payment.component";
|
||||||
import { BillingHistoryViewComponent } from "../billing/settings/billing-history-view.component";
|
import { BillingHistoryViewComponent } from "../billing/settings/billing-history-view.component";
|
||||||
|
@ -125,6 +126,9 @@ import { SharedModule } from "./shared.module";
|
||||||
DynamicAvatarComponent,
|
DynamicAvatarComponent,
|
||||||
EnvironmentSelectorModule,
|
EnvironmentSelectorModule,
|
||||||
AccountFingerprintComponent,
|
AccountFingerprintComponent,
|
||||||
|
|
||||||
|
// To be removed when OrganizationPlansComponent is moved to its own module (see AC-1453)
|
||||||
|
SecretsManagerBillingModule,
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
AcceptEmergencyComponent,
|
AcceptEmergencyComponent,
|
||||||
|
|
|
@ -23,6 +23,7 @@ import {
|
||||||
LinkModule,
|
LinkModule,
|
||||||
MenuModule,
|
MenuModule,
|
||||||
MultiSelectModule,
|
MultiSelectModule,
|
||||||
|
ProgressModule,
|
||||||
RadioButtonModule,
|
RadioButtonModule,
|
||||||
SelectModule,
|
SelectModule,
|
||||||
TableModule,
|
TableModule,
|
||||||
|
@ -69,6 +70,7 @@ import "./locales";
|
||||||
LinkModule,
|
LinkModule,
|
||||||
MenuModule,
|
MenuModule,
|
||||||
MultiSelectModule,
|
MultiSelectModule,
|
||||||
|
ProgressModule,
|
||||||
RadioButtonModule,
|
RadioButtonModule,
|
||||||
TableModule,
|
TableModule,
|
||||||
TabsModule,
|
TabsModule,
|
||||||
|
@ -103,6 +105,7 @@ import "./locales";
|
||||||
LinkModule,
|
LinkModule,
|
||||||
MenuModule,
|
MenuModule,
|
||||||
MultiSelectModule,
|
MultiSelectModule,
|
||||||
|
ProgressModule,
|
||||||
RadioButtonModule,
|
RadioButtonModule,
|
||||||
SelectModule,
|
SelectModule,
|
||||||
TableModule,
|
TableModule,
|
||||||
|
|
|
@ -3323,6 +3323,9 @@
|
||||||
"limitSubscriptionDesc": {
|
"limitSubscriptionDesc": {
|
||||||
"message": "Set a seat limit for your subscription. Once this limit is reached, you will not be able to invite new members."
|
"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": {
|
"maxSeatLimit": {
|
||||||
"message": "Seat Limit (optional)",
|
"message": "Seat Limit (optional)",
|
||||||
"description": "Upper limit of seats to allow through autoscaling"
|
"description": "Upper limit of seats to allow through autoscaling"
|
||||||
|
@ -6617,12 +6620,12 @@
|
||||||
"changeKdfLoggedOutWarning": {
|
"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."
|
"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": {
|
"secretsManager": {
|
||||||
"message": "Secrets Manager"
|
"message": "Secrets Manager"
|
||||||
},
|
},
|
||||||
|
"secretsManagerBeta": {
|
||||||
|
"message": "Secrets Manager Beta"
|
||||||
|
},
|
||||||
"secretsManagerAccessDescription": {
|
"secretsManagerAccessDescription": {
|
||||||
"message": "Activate user access to Secrets Manager."
|
"message": "Activate user access to Secrets Manager."
|
||||||
},
|
},
|
||||||
|
@ -6955,5 +6958,99 @@
|
||||||
},
|
},
|
||||||
"selectedRegionFlag": {
|
"selectedRegionFlag": {
|
||||||
"message": "Selected region flag"
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,9 +3,11 @@ import { OrganizationSsoRequest } from "../../../auth/models/request/organizatio
|
||||||
import { SecretVerificationRequest } from "../../../auth/models/request/secret-verification.request";
|
import { SecretVerificationRequest } from "../../../auth/models/request/secret-verification.request";
|
||||||
import { ApiKeyResponse } from "../../../auth/models/response/api-key.response";
|
import { ApiKeyResponse } from "../../../auth/models/response/api-key.response";
|
||||||
import { OrganizationSsoResponse } from "../../../auth/models/response/organization-sso.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 { OrganizationSubscriptionUpdateRequest } from "../../../billing/models/request/organization-subscription-update.request";
|
||||||
import { OrganizationTaxInfoUpdateRequest } from "../../../billing/models/request/organization-tax-info-update.request";
|
import { OrganizationTaxInfoUpdateRequest } from "../../../billing/models/request/organization-tax-info-update.request";
|
||||||
import { PaymentRequest } from "../../../billing/models/request/payment.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 { BillingResponse } from "../../../billing/models/response/billing.response";
|
||||||
import { OrganizationSubscriptionResponse } from "../../../billing/models/response/organization-subscription.response";
|
import { OrganizationSubscriptionResponse } from "../../../billing/models/response/organization-subscription.response";
|
||||||
import { PaymentResponse } from "../../../billing/models/response/payment.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 { VerifyBankRequest } from "../../../models/request/verify-bank.request";
|
||||||
import { ListResponse } from "../../../models/response/list.response";
|
import { ListResponse } from "../../../models/response/list.response";
|
||||||
import { OrganizationApiKeyType } from "../../enums";
|
import { OrganizationApiKeyType } from "../../enums";
|
||||||
import { OrganizationEnrollSecretsManagerRequest } from "../../models/request/organization/organization-enroll-secrets-manager.request";
|
|
||||||
import { OrganizationCreateRequest } from "../../models/request/organization-create.request";
|
import { OrganizationCreateRequest } from "../../models/request/organization-create.request";
|
||||||
import { OrganizationKeysRequest } from "../../models/request/organization-keys.request";
|
import { OrganizationKeysRequest } from "../../models/request/organization-keys.request";
|
||||||
import { OrganizationUpdateRequest } from "../../models/request/organization-update.request";
|
import { OrganizationUpdateRequest } from "../../models/request/organization-update.request";
|
||||||
|
@ -37,7 +38,14 @@ export class OrganizationApiServiceAbstraction {
|
||||||
save: (id: string, request: OrganizationUpdateRequest) => Promise<OrganizationResponse>;
|
save: (id: string, request: OrganizationUpdateRequest) => Promise<OrganizationResponse>;
|
||||||
updatePayment: (id: string, request: PaymentRequest) => Promise<void>;
|
updatePayment: (id: string, request: PaymentRequest) => Promise<void>;
|
||||||
upgrade: (id: string, request: OrganizationUpgradeRequest) => Promise<PaymentResponse>;
|
upgrade: (id: string, request: OrganizationUpgradeRequest) => Promise<PaymentResponse>;
|
||||||
updateSubscription: (id: string, request: OrganizationSubscriptionUpdateRequest) => Promise<void>;
|
updatePasswordManagerSeats: (
|
||||||
|
id: string,
|
||||||
|
request: OrganizationSubscriptionUpdateRequest
|
||||||
|
) => Promise<void>;
|
||||||
|
updateSecretsManagerSubscription: (
|
||||||
|
id: string,
|
||||||
|
request: OrganizationSmSubscriptionUpdateRequest
|
||||||
|
) => Promise<void>;
|
||||||
updateSeats: (id: string, request: SeatRequest) => Promise<PaymentResponse>;
|
updateSeats: (id: string, request: SeatRequest) => Promise<PaymentResponse>;
|
||||||
updateStorage: (id: string, request: StorageRequest) => Promise<PaymentResponse>;
|
updateStorage: (id: string, request: StorageRequest) => Promise<PaymentResponse>;
|
||||||
verifyBank: (id: string, request: VerifyBankRequest) => Promise<void>;
|
verifyBank: (id: string, request: VerifyBankRequest) => Promise<void>;
|
||||||
|
@ -60,8 +68,5 @@ export class OrganizationApiServiceAbstraction {
|
||||||
getSso: (id: string) => Promise<OrganizationSsoResponse>;
|
getSso: (id: string) => Promise<OrganizationSsoResponse>;
|
||||||
updateSso: (id: string, request: OrganizationSsoRequest) => Promise<OrganizationSsoResponse>;
|
updateSso: (id: string, request: OrganizationSsoRequest) => Promise<OrganizationSsoResponse>;
|
||||||
selfHostedSyncLicense: (id: string) => Promise<void>;
|
selfHostedSyncLicense: (id: string) => Promise<void>;
|
||||||
updateEnrollSecretsManager: (
|
subscribeToSecretsManager: (id: string, request: SecretsManagerSubscribeRequest) => Promise<void>;
|
||||||
id: string,
|
|
||||||
request: OrganizationEnrollSecretsManagerRequest
|
|
||||||
) => Promise<void>;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,7 @@ export class OrganizationData {
|
||||||
useCustomPermissions: boolean;
|
useCustomPermissions: boolean;
|
||||||
useResetPassword: boolean;
|
useResetPassword: boolean;
|
||||||
useSecretsManager: boolean;
|
useSecretsManager: boolean;
|
||||||
|
usePasswordManager: boolean;
|
||||||
useActivateAutofillPolicy: boolean;
|
useActivateAutofillPolicy: boolean;
|
||||||
selfHost: boolean;
|
selfHost: boolean;
|
||||||
usersGetPremium: boolean;
|
usersGetPremium: boolean;
|
||||||
|
@ -74,6 +75,7 @@ export class OrganizationData {
|
||||||
this.useCustomPermissions = response.useCustomPermissions;
|
this.useCustomPermissions = response.useCustomPermissions;
|
||||||
this.useResetPassword = response.useResetPassword;
|
this.useResetPassword = response.useResetPassword;
|
||||||
this.useSecretsManager = response.useSecretsManager;
|
this.useSecretsManager = response.useSecretsManager;
|
||||||
|
this.usePasswordManager = response.usePasswordManager;
|
||||||
this.useActivateAutofillPolicy = response.useActivateAutofillPolicy;
|
this.useActivateAutofillPolicy = response.useActivateAutofillPolicy;
|
||||||
this.selfHost = response.selfHost;
|
this.selfHost = response.selfHost;
|
||||||
this.usersGetPremium = response.usersGetPremium;
|
this.usersGetPremium = response.usersGetPremium;
|
||||||
|
|
|
@ -31,6 +31,7 @@ export class Organization {
|
||||||
useCustomPermissions: boolean;
|
useCustomPermissions: boolean;
|
||||||
useResetPassword: boolean;
|
useResetPassword: boolean;
|
||||||
useSecretsManager: boolean;
|
useSecretsManager: boolean;
|
||||||
|
usePasswordManager: boolean;
|
||||||
useActivateAutofillPolicy: boolean;
|
useActivateAutofillPolicy: boolean;
|
||||||
selfHost: boolean;
|
selfHost: boolean;
|
||||||
usersGetPremium: boolean;
|
usersGetPremium: boolean;
|
||||||
|
@ -87,6 +88,7 @@ export class Organization {
|
||||||
this.useCustomPermissions = obj.useCustomPermissions;
|
this.useCustomPermissions = obj.useCustomPermissions;
|
||||||
this.useResetPassword = obj.useResetPassword;
|
this.useResetPassword = obj.useResetPassword;
|
||||||
this.useSecretsManager = obj.useSecretsManager;
|
this.useSecretsManager = obj.useSecretsManager;
|
||||||
|
this.usePasswordManager = obj.usePasswordManager;
|
||||||
this.useActivateAutofillPolicy = obj.useActivateAutofillPolicy;
|
this.useActivateAutofillPolicy = obj.useActivateAutofillPolicy;
|
||||||
this.selfHost = obj.selfHost;
|
this.selfHost = obj.selfHost;
|
||||||
this.usersGetPremium = obj.usersGetPremium;
|
this.usersGetPremium = obj.usersGetPremium;
|
||||||
|
|
|
@ -23,4 +23,8 @@ export class OrganizationCreateRequest {
|
||||||
billingAddressState: string;
|
billingAddressState: string;
|
||||||
billingAddressPostalCode: string;
|
billingAddressPostalCode: string;
|
||||||
billingAddressCountry: string;
|
billingAddressCountry: string;
|
||||||
|
|
||||||
|
useSecretsManager: boolean;
|
||||||
|
additionalSmSeats: number;
|
||||||
|
additionalServiceAccounts: number;
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,4 +11,8 @@ export class OrganizationUpgradeRequest {
|
||||||
billingAddressCountry: string;
|
billingAddressCountry: string;
|
||||||
billingAddressPostalCode: string;
|
billingAddressPostalCode: string;
|
||||||
keys: OrganizationKeysRequest;
|
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;
|
businessTaxNumber: string;
|
||||||
billingEmail: string;
|
billingEmail: string;
|
||||||
plan: PlanResponse;
|
plan: PlanResponse;
|
||||||
|
secretsManagerPlan: PlanResponse;
|
||||||
planType: PlanType;
|
planType: PlanType;
|
||||||
seats: number;
|
seats: number;
|
||||||
maxAutoscaleSeats: number;
|
maxAutoscaleSeats: number;
|
||||||
|
@ -27,6 +28,11 @@ export class OrganizationResponse extends BaseResponse {
|
||||||
useResetPassword: boolean;
|
useResetPassword: boolean;
|
||||||
useSecretsManager: boolean;
|
useSecretsManager: boolean;
|
||||||
hasPublicAndPrivateKeys: boolean;
|
hasPublicAndPrivateKeys: boolean;
|
||||||
|
usePasswordManager: boolean;
|
||||||
|
smSeats?: number;
|
||||||
|
smServiceAccounts?: number;
|
||||||
|
maxAutoscaleSmSeats?: number;
|
||||||
|
maxAutoscaleSmServiceAccounts?: number;
|
||||||
|
|
||||||
constructor(response: any) {
|
constructor(response: any) {
|
||||||
super(response);
|
super(response);
|
||||||
|
@ -39,8 +45,14 @@ export class OrganizationResponse extends BaseResponse {
|
||||||
this.businessCountry = this.getResponseProperty("BusinessCountry");
|
this.businessCountry = this.getResponseProperty("BusinessCountry");
|
||||||
this.businessTaxNumber = this.getResponseProperty("BusinessTaxNumber");
|
this.businessTaxNumber = this.getResponseProperty("BusinessTaxNumber");
|
||||||
this.billingEmail = this.getResponseProperty("BillingEmail");
|
this.billingEmail = this.getResponseProperty("BillingEmail");
|
||||||
|
|
||||||
const plan = this.getResponseProperty("Plan");
|
const plan = this.getResponseProperty("Plan");
|
||||||
this.plan = plan == null ? null : new PlanResponse(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.planType = this.getResponseProperty("PlanType");
|
||||||
this.seats = this.getResponseProperty("Seats");
|
this.seats = this.getResponseProperty("Seats");
|
||||||
this.maxAutoscaleSeats = this.getResponseProperty("MaxAutoscaleSeats");
|
this.maxAutoscaleSeats = this.getResponseProperty("MaxAutoscaleSeats");
|
||||||
|
@ -55,5 +67,10 @@ export class OrganizationResponse extends BaseResponse {
|
||||||
this.useResetPassword = this.getResponseProperty("UseResetPassword");
|
this.useResetPassword = this.getResponseProperty("UseResetPassword");
|
||||||
this.useSecretsManager = this.getResponseProperty("UseSecretsManager");
|
this.useSecretsManager = this.getResponseProperty("UseSecretsManager");
|
||||||
this.hasPublicAndPrivateKeys = this.getResponseProperty("HasPublicAndPrivateKeys");
|
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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@ export class ProfileOrganizationResponse extends BaseResponse {
|
||||||
useCustomPermissions: boolean;
|
useCustomPermissions: boolean;
|
||||||
useResetPassword: boolean;
|
useResetPassword: boolean;
|
||||||
useSecretsManager: boolean;
|
useSecretsManager: boolean;
|
||||||
|
usePasswordManager: boolean;
|
||||||
useActivateAutofillPolicy: boolean;
|
useActivateAutofillPolicy: boolean;
|
||||||
selfHost: boolean;
|
selfHost: boolean;
|
||||||
usersGetPremium: boolean;
|
usersGetPremium: boolean;
|
||||||
|
@ -65,6 +66,7 @@ export class ProfileOrganizationResponse extends BaseResponse {
|
||||||
this.useCustomPermissions = this.getResponseProperty("UseCustomPermissions") ?? false;
|
this.useCustomPermissions = this.getResponseProperty("UseCustomPermissions") ?? false;
|
||||||
this.useResetPassword = this.getResponseProperty("UseResetPassword");
|
this.useResetPassword = this.getResponseProperty("UseResetPassword");
|
||||||
this.useSecretsManager = this.getResponseProperty("UseSecretsManager");
|
this.useSecretsManager = this.getResponseProperty("UseSecretsManager");
|
||||||
|
this.usePasswordManager = this.getResponseProperty("UsePasswordManager");
|
||||||
this.useActivateAutofillPolicy = this.getResponseProperty("UseActivateAutofillPolicy");
|
this.useActivateAutofillPolicy = this.getResponseProperty("UseActivateAutofillPolicy");
|
||||||
this.selfHost = this.getResponseProperty("SelfHost");
|
this.selfHost = this.getResponseProperty("SelfHost");
|
||||||
this.usersGetPremium = this.getResponseProperty("UsersGetPremium");
|
this.usersGetPremium = this.getResponseProperty("UsersGetPremium");
|
||||||
|
|
|
@ -4,9 +4,11 @@ import { OrganizationSsoRequest } from "../../../auth/models/request/organizatio
|
||||||
import { SecretVerificationRequest } from "../../../auth/models/request/secret-verification.request";
|
import { SecretVerificationRequest } from "../../../auth/models/request/secret-verification.request";
|
||||||
import { ApiKeyResponse } from "../../../auth/models/response/api-key.response";
|
import { ApiKeyResponse } from "../../../auth/models/response/api-key.response";
|
||||||
import { OrganizationSsoResponse } from "../../../auth/models/response/organization-sso.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 { OrganizationSubscriptionUpdateRequest } from "../../../billing/models/request/organization-subscription-update.request";
|
||||||
import { OrganizationTaxInfoUpdateRequest } from "../../../billing/models/request/organization-tax-info-update.request";
|
import { OrganizationTaxInfoUpdateRequest } from "../../../billing/models/request/organization-tax-info-update.request";
|
||||||
import { PaymentRequest } from "../../../billing/models/request/payment.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 { BillingResponse } from "../../../billing/models/response/billing.response";
|
||||||
import { OrganizationSubscriptionResponse } from "../../../billing/models/response/organization-subscription.response";
|
import { OrganizationSubscriptionResponse } from "../../../billing/models/response/organization-subscription.response";
|
||||||
import { PaymentResponse } from "../../../billing/models/response/payment.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 { SyncService } from "../../../vault/abstractions/sync/sync.service.abstraction";
|
||||||
import { OrganizationApiServiceAbstraction } from "../../abstractions/organization/organization-api.service.abstraction";
|
import { OrganizationApiServiceAbstraction } from "../../abstractions/organization/organization-api.service.abstraction";
|
||||||
import { OrganizationApiKeyType } from "../../enums";
|
import { OrganizationApiKeyType } from "../../enums";
|
||||||
import { OrganizationEnrollSecretsManagerRequest } from "../../models/request/organization/organization-enroll-secrets-manager.request";
|
|
||||||
import { OrganizationCreateRequest } from "../../models/request/organization-create.request";
|
import { OrganizationCreateRequest } from "../../models/request/organization-create.request";
|
||||||
import { OrganizationKeysRequest } from "../../models/request/organization-keys.request";
|
import { OrganizationKeysRequest } from "../../models/request/organization-keys.request";
|
||||||
import { OrganizationUpdateRequest } from "../../models/request/organization-update.request";
|
import { OrganizationUpdateRequest } from "../../models/request/organization-update.request";
|
||||||
|
@ -120,7 +121,7 @@ export class OrganizationApiService implements OrganizationApiServiceAbstraction
|
||||||
return new PaymentResponse(r);
|
return new PaymentResponse(r);
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateSubscription(
|
async updatePasswordManagerSeats(
|
||||||
id: string,
|
id: string,
|
||||||
request: OrganizationSubscriptionUpdateRequest
|
request: OrganizationSubscriptionUpdateRequest
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
@ -133,6 +134,19 @@ export class OrganizationApiService implements OrganizationApiServiceAbstraction
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async updateSecretsManagerSubscription(
|
||||||
|
id: string,
|
||||||
|
request: OrganizationSmSubscriptionUpdateRequest
|
||||||
|
): Promise<void> {
|
||||||
|
return this.apiService.send(
|
||||||
|
"POST",
|
||||||
|
"/organizations/" + id + "/sm-subscription",
|
||||||
|
request,
|
||||||
|
true,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async updateSeats(id: string, request: SeatRequest): Promise<PaymentResponse> {
|
async updateSeats(id: string, request: SeatRequest): Promise<PaymentResponse> {
|
||||||
const r = await this.apiService.send(
|
const r = await this.apiService.send(
|
||||||
"POST",
|
"POST",
|
||||||
|
@ -294,13 +308,16 @@ export class OrganizationApiService implements OrganizationApiServiceAbstraction
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateEnrollSecretsManager(id: string, request: OrganizationEnrollSecretsManagerRequest) {
|
async subscribeToSecretsManager(
|
||||||
await this.apiService.send(
|
id: string,
|
||||||
|
request: SecretsManagerSubscribeRequest
|
||||||
|
): Promise<void> {
|
||||||
|
return await this.apiService.send(
|
||||||
"POST",
|
"POST",
|
||||||
"/organizations/" + id + "/enroll-secrets-manager",
|
"/organizations/" + id + "/subscribe-secrets-manager",
|
||||||
request,
|
request,
|
||||||
true,
|
true,
|
||||||
true
|
false
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
export enum BitwardenProductType {
|
||||||
|
PasswordManager = 0,
|
||||||
|
SecretsManager = 1,
|
||||||
|
}
|
|
@ -2,3 +2,4 @@ export * from "./payment-method-type.enum";
|
||||||
export * from "./plan-sponsorship-type.enum";
|
export * from "./plan-sponsorship-type.enum";
|
||||||
export * from "./plan-type.enum";
|
export * from "./plan-type.enum";
|
||||||
export * from "./transaction-type.enum";
|
export * from "./transaction-type.enum";
|
||||||
|
export * from "./bitwarden-product-type.enum";
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -1,3 +1,23 @@
|
||||||
export class OrganizationSubscriptionUpdateRequest {
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
export class SecretsManagerSubscribeRequest {
|
||||||
|
additionalSmSeats: number;
|
||||||
|
additionalServiceAccounts: number;
|
||||||
|
}
|
|
@ -1,10 +1,11 @@
|
||||||
import { ProductType } from "../../../enums";
|
import { ProductType } from "../../../enums";
|
||||||
import { BaseResponse } from "../../../models/response/base.response";
|
import { BaseResponse } from "../../../models/response/base.response";
|
||||||
import { PlanType } from "../../enums";
|
import { BitwardenProductType, PlanType } from "../../enums";
|
||||||
|
|
||||||
export class PlanResponse extends BaseResponse {
|
export class PlanResponse extends BaseResponse {
|
||||||
type: PlanType;
|
type: PlanType;
|
||||||
product: ProductType;
|
product: ProductType;
|
||||||
|
bitwardenProduct: BitwardenProductType;
|
||||||
name: string;
|
name: string;
|
||||||
isAnnual: boolean;
|
isAnnual: boolean;
|
||||||
nameLocalizationKey: string;
|
nameLocalizationKey: string;
|
||||||
|
@ -48,6 +49,15 @@ export class PlanResponse extends BaseResponse {
|
||||||
additionalStoragePricePerGb: number;
|
additionalStoragePricePerGb: number;
|
||||||
premiumAccessOptionPrice: number;
|
premiumAccessOptionPrice: number;
|
||||||
|
|
||||||
|
// SM only
|
||||||
|
additionalPricePerServiceAccount: number;
|
||||||
|
baseServiceAccount: number;
|
||||||
|
maxServiceAccount: number;
|
||||||
|
hasAdditionalServiceAccountOption: boolean;
|
||||||
|
maxProjects: number;
|
||||||
|
maxAdditionalServiceAccounts: number;
|
||||||
|
stripeServiceAccountPlanId: string;
|
||||||
|
|
||||||
constructor(response: any) {
|
constructor(response: any) {
|
||||||
super(response);
|
super(response);
|
||||||
this.type = this.getResponseProperty("Type");
|
this.type = this.getResponseProperty("Type");
|
||||||
|
@ -90,5 +100,18 @@ export class PlanResponse extends BaseResponse {
|
||||||
this.seatPrice = this.getResponseProperty("SeatPrice");
|
this.seatPrice = this.getResponseProperty("SeatPrice");
|
||||||
this.additionalStoragePricePerGb = this.getResponseProperty("AdditionalStoragePricePerGb");
|
this.additionalStoragePricePerGb = this.getResponseProperty("AdditionalStoragePricePerGb");
|
||||||
this.premiumAccessOptionPrice = this.getResponseProperty("PremiumAccessOptionPrice");
|
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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { BaseResponse } from "../../../models/response/base.response";
|
import { BaseResponse } from "../../../models/response/base.response";
|
||||||
|
import { BitwardenProductType } from "../../enums";
|
||||||
|
|
||||||
export class SubscriptionResponse extends BaseResponse {
|
export class SubscriptionResponse extends BaseResponse {
|
||||||
storageName: string;
|
storageName: string;
|
||||||
|
@ -62,6 +63,8 @@ export class BillingSubscriptionItemResponse extends BaseResponse {
|
||||||
quantity: number;
|
quantity: number;
|
||||||
interval: string;
|
interval: string;
|
||||||
sponsoredSubscriptionItem: boolean;
|
sponsoredSubscriptionItem: boolean;
|
||||||
|
addonSubscriptionItem: boolean;
|
||||||
|
bitwardenProduct: BitwardenProductType;
|
||||||
|
|
||||||
constructor(response: any) {
|
constructor(response: any) {
|
||||||
super(response);
|
super(response);
|
||||||
|
@ -70,6 +73,8 @@ export class BillingSubscriptionItemResponse extends BaseResponse {
|
||||||
this.quantity = this.getResponseProperty("Quantity");
|
this.quantity = this.getResponseProperty("Quantity");
|
||||||
this.interval = this.getResponseProperty("Interval");
|
this.interval = this.getResponseProperty("Interval");
|
||||||
this.sponsoredSubscriptionItem = this.getResponseProperty("SponsoredSubscriptionItem");
|
this.sponsoredSubscriptionItem = this.getResponseProperty("SponsoredSubscriptionItem");
|
||||||
|
this.addonSubscriptionItem = this.getResponseProperty("AddonSubscriptionItem");
|
||||||
|
this.bitwardenProduct = this.getResponseProperty("BitwardenProduct");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,4 +2,5 @@ export enum FeatureFlag {
|
||||||
DisplayEuEnvironmentFlag = "display-eu-environment",
|
DisplayEuEnvironmentFlag = "display-eu-environment",
|
||||||
DisplayLowKdfIterationWarningFlag = "display-kdf-iteration-warning",
|
DisplayLowKdfIterationWarningFlag = "display-kdf-iteration-warning",
|
||||||
TrustedDeviceEncryption = "trusted-device-encryption",
|
TrustedDeviceEncryption = "trusted-device-encryption",
|
||||||
|
SecretsManagerBilling = "sm-ga-billing",
|
||||||
}
|
}
|
||||||
|
|
|
@ -881,7 +881,7 @@ export class ApiService implements ApiServiceAbstraction {
|
||||||
// Plan APIs
|
// Plan APIs
|
||||||
|
|
||||||
async getPlans(): Promise<ListResponse<PlanResponse>> {
|
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);
|
return new ListResponse(r, PlanResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue