mirror of
https://github.com/bitwarden/browser.git
synced 2024-09-29 04:17:41 +02:00
[AC-1512] Feature: Secrets Manager billing - round 2 (#5854)
* [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-1423] Simply sort function * [AC-1423] Remove header border * [AC-1423] Make "Password Manager" the default fallback for product name * [AC-1420] Add Secrets Manager subscribe component (#5617) * [AC-1418] Add secrets manager manage subscription component (#5661) * [AC-1423] Add minWidth input to bit-progress component * [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-1423] Simply sort function * [AC-1423] Remove header border * [AC-1423] Remove redundant condition * [AC-1423] Remove ineffective div * [AC-1423] Make "Password Manager" the default fallback for product name * Revert "[AC-1423] Add minWidth input to bit-progress component" This reverts commit95b2223a30
. * [AC-1423] Remove minWidth attribute * [AC-1423] Switch to AddonProductType enum instead of boolean * Revert "[AC-1423] Switch to AddonProductType enum instead of boolean" This reverts commit204f64b4e7
. * [AC-1423] Tweak sorting comment * [AC-1418] Add initial SecretsManagerAdjustSubscription component * [AC-1418] Add initial SM adjustment form * [AC-1418] Adjust organization-subscription-update.request.ts to support both PM and SM * [AC-1418] Rename service account fields in the options interface * [AC-1418] Add api service call to update SM subscription * [AC-1418] Cleanup form html * [AC-1418] Add missing SM plan properties * [AC-1418] Add SM subscription adjust form and logic to hide it * [AC-1418] Add better docs to options interface * [AC-1418] Fix conflicting required/optional labels for auto-scaling limits * [AC-1418] Adjust labels and appearance to better match design * [AC-1418] Use the SM plan for billing interval * [AC-1418] Hide SM billing adjustment component behind feature flag * [AC-1418] Update request model to match server * [AC-1418] Cleanup BitwardenProductType after merge Add to barrel file and update applicable imports. * [AC-1418] Revert change to update PM subscription request model * [AC-1418] Add new update SM subscription request model * [AC-1418] Add new service method to update SM subscription * [AC-1418] Use new model and service method * [AC-1418] Cleanup SM subscription UI flags * [AC-1418] Move SM adjust subscription component into SM billing module * [AC-1418] Update SM seat count minimum to 1 * [AC-1418] Add missing currency codes * [AC-1418] Simplify monthly price calculation * [AC-1418] Increase PM adjust subscription form input width * [AC-1418] Add check for null subscription --------- Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com> * add the 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 --------- Co-authored-by: Shane Melton <smelton@bitwarden.com> * [AC-1473] SM beta ending callout (#5719) * [AC-1423] Add minWidth input to bit-progress component * [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-1423] Simply sort function * [AC-1423] Remove header border * [AC-1423] Remove redundant condition * [AC-1423] Remove ineffective div * [AC-1423] Make "Password Manager" the default fallback for product name * Revert "[AC-1423] Add minWidth input to bit-progress component" This reverts commit95b2223a30
. * [AC-1423] Remove minWidth attribute * [AC-1423] Switch to AddonProductType enum instead of boolean * Revert "[AC-1423] Switch to AddonProductType enum instead of boolean" This reverts commit204f64b4e7
. * [AC-1423] Tweak sorting comment * [AC-1418] Add initial SecretsManagerAdjustSubscription component * [AC-1418] Add initial SM adjustment form * [AC-1418] Adjust organization-subscription-update.request.ts to support both PM and SM * [AC-1418] Rename service account fields in the options interface * [AC-1418] Add api service call to update SM subscription * [AC-1418] Cleanup form html * [AC-1418] Add missing SM plan properties * [AC-1418] Add SM subscription adjust form and logic to hide it * [AC-1418] Add better docs to options interface * [AC-1418] Fix conflicting required/optional labels for auto-scaling limits * [AC-1418] Adjust labels and appearance to better match design * [AC-1418] Use the SM plan for billing interval * [AC-1418] Hide SM billing adjustment component behind feature flag * [AC-1418] Update request model to match server * [AC-1418] Cleanup BitwardenProductType after merge Add to barrel file and update applicable imports. * [AC-1418] Revert change to update PM subscription request model * [AC-1418] Add new update SM subscription request model * [AC-1418] Add new service method to update SM subscription * [AC-1418] Use new model and service method * [AC-1418] Cleanup SM subscription UI flags * [AC-1418] Move SM adjust subscription component into SM billing module * [AC-1418] Update SM seat count minimum to 1 * [AC-1418] Add missing currency codes * [AC-1418] Simplify monthly price calculation * add daysRemaining util function and unit tests * [AC-1474] update organization models to include SM beta flag * add SM beta callout to org subscription page * update messages.json * remove beta field from profile org response * improve daysRemaining code clarity * set SM beta in org model constructor * tweak free SM row visibility * refactor callout description * Revert "remove beta field from profile org response" This reverts commit6c6249e1ec
. * fix dates * [AC-1468]: hide adjust SM component if beta user * add sm beta field to org sub response; remove everywhere else * fix copy --------- 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> * [AC-1531] Fix SM subscribe component not showing in free org billing tab (#5848) Also: * Fix spacing in layout * Send zero values for free plans * Fix: properly delete enroll component * remove the beta end message for free org (#5877) * [AC-1458] Update local organization data after subscribing to Secrets Manager (#5888) * [AC-1567] Fix max additional service account cost estimate (#5923) * Fix max additional service account cost estimate * Update i18n string ref * Make i18n string keys consistent * [AC-1461] Secrets Manager seat autoscaling cleanup (#5924) * Remove unused return value from putOrganizationUserBulkEnableSecretsManager * Fix service account limit validator (#5926) * Updated Utils.daysRemaining method to calculate result using Math.floor and updated unit tests. --------- Co-authored-by: Shane Melton <smelton@bitwarden.com> Co-authored-by: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Co-authored-by: Rui Tome <rtome@bitwarden.com> Co-authored-by: Will Martin <contact@willmartian.com> Co-authored-by: Álison Fernandes <vvolkgang@users.noreply.github.com> Co-authored-by: Rui Tomé <108268980+r-tome@users.noreply.github.com>
This commit is contained in:
parent
dad6fedebd
commit
b89f31101f
@ -9,7 +9,7 @@ import { SettingsService as SettingsServiceAbstraction } from "@bitwarden/common
|
|||||||
import { TotpService as TotpServiceAbstraction } from "@bitwarden/common/abstractions/totp.service";
|
import { TotpService as TotpServiceAbstraction } from "@bitwarden/common/abstractions/totp.service";
|
||||||
import { VaultTimeoutService as VaultTimeoutServiceAbstraction } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeout.service";
|
import { VaultTimeoutService as VaultTimeoutServiceAbstraction } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeout.service";
|
||||||
import { VaultTimeoutSettingsService as VaultTimeoutSettingsServiceAbstraction } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeoutSettings.service";
|
import { VaultTimeoutSettingsService as VaultTimeoutSettingsServiceAbstraction } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeoutSettings.service";
|
||||||
import { InternalOrganizationService as InternalOrganizationServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
import { InternalOrganizationServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||||
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
|
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
|
||||||
import { InternalPolicyService as InternalPolicyServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
import { InternalPolicyService as InternalPolicyServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||||
import { ProviderService as ProviderServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider.service";
|
import { ProviderService as ProviderServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider.service";
|
||||||
|
@ -87,13 +87,32 @@
|
|||||||
<td bitCell>{{ "passwordManager" | i18n }} - {{ "freeOrganization" | i18n }}</td>
|
<td bitCell>{{ "passwordManager" | i18n }} - {{ "freeOrganization" | i18n }}</td>
|
||||||
<td bitCell class="tw-text-right">{{ "free" | i18n }}</td>
|
<td bitCell class="tw-text-right">{{ "free" | i18n }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr bitRow *ngIf="userOrg.useSecretsManager">
|
<tr bitRow *ngIf="userOrg.useSecretsManager && !sub.secretsManagerBeta">
|
||||||
<td bitCell>{{ "secretsManager" | i18n }} - {{ "freeOrganization" | i18n }}</td>
|
<td bitCell>{{ "secretsManager" | i18n }} - {{ "freeOrganization" | i18n }}</td>
|
||||||
<td bitCell class="tw-text-right">{{ "free" | i18n }}</td>
|
<td bitCell class="tw-text-right">{{ "free" | i18n }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
<tr bitRow *ngIf="sub.secretsManagerBeta">
|
||||||
|
<td bitCell>
|
||||||
|
{{ "secretsManager" | i18n }} -
|
||||||
|
{{ "beta" | i18n }}
|
||||||
|
({{ "annually" | i18n }}) @
|
||||||
|
{{ 0 | currency : "$" }}
|
||||||
|
<span bitBadge badgeType="warning" class="tw-ml-2">{{
|
||||||
|
"betaEnding" | i18n | uppercase
|
||||||
|
}}</span>
|
||||||
|
</td>
|
||||||
|
<td bitCell class="tw-text-right">{{ 0 | currency : "$" }} /{{ "year" | i18n }}</td>
|
||||||
|
</tr>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</bit-table>
|
</bit-table>
|
||||||
|
<bit-callout
|
||||||
|
*ngIf="sub.secretsManagerBeta && !userOrg.isFreeOrg"
|
||||||
|
type="warning"
|
||||||
|
class="tw-mt-4 tw-block"
|
||||||
|
>
|
||||||
|
{{ smBetaEndedDesc }}
|
||||||
|
</bit-callout>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { DatePipe } from "@angular/common";
|
||||||
import { Component, OnDestroy, OnInit } from "@angular/core";
|
import { Component, OnDestroy, OnInit } from "@angular/core";
|
||||||
import { ActivatedRoute } from "@angular/router";
|
import { ActivatedRoute } from "@angular/router";
|
||||||
import { concatMap, Subject, takeUntil } from "rxjs";
|
import { concatMap, Subject, takeUntil } from "rxjs";
|
||||||
@ -17,6 +18,7 @@ import { ConfigServiceAbstraction } from "@bitwarden/common/platform/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";
|
||||||
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
BillingSyncApiKeyComponent,
|
BillingSyncApiKeyComponent,
|
||||||
@ -45,6 +47,9 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
|
|||||||
firstLoaded = false;
|
firstLoaded = false;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
|
|
||||||
|
private readonly _smBetaEndingDate = new Date(2023, 7, 25);
|
||||||
|
private readonly _smGracePeriodEndingDate = new Date(2023, 9, 24);
|
||||||
|
|
||||||
private destroy$ = new Subject<void>();
|
private destroy$ = new Subject<void>();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@ -57,7 +62,8 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
|
|||||||
private organizationApiService: OrganizationApiServiceAbstraction,
|
private organizationApiService: OrganizationApiServiceAbstraction,
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
private dialogService: DialogServiceAbstraction,
|
private dialogService: DialogServiceAbstraction,
|
||||||
private configService: ConfigServiceAbstraction
|
private configService: ConfigServiceAbstraction,
|
||||||
|
private datePipe: DatePipe
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
@ -122,6 +128,7 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
|
|||||||
this.userOrg.useSecretsManager &&
|
this.userOrg.useSecretsManager &&
|
||||||
this.subscription != null &&
|
this.subscription != null &&
|
||||||
this.sub.secretsManagerPlan?.hasAdditionalSeatsOption &&
|
this.sub.secretsManagerPlan?.hasAdditionalSeatsOption &&
|
||||||
|
!this.sub.secretsManagerBeta &&
|
||||||
!this.subscription.cancelled &&
|
!this.subscription.cancelled &&
|
||||||
!this.subscriptionMarkedForCancel;
|
!this.subscriptionMarkedForCancel;
|
||||||
|
|
||||||
@ -256,6 +263,14 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get smBetaEndedDesc() {
|
||||||
|
return this.i18nService.translate(
|
||||||
|
"smBetaEndedDesc",
|
||||||
|
this.datePipe.transform(this._smBetaEndingDate),
|
||||||
|
Utils.daysRemaining(this._smGracePeriodEndingDate).toString()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
cancel = async () => {
|
cancel = async () => {
|
||||||
if (this.loading) {
|
if (this.loading) {
|
||||||
return;
|
return;
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
<bit-hint>
|
<bit-hint>
|
||||||
<strong>{{ "total" | i18n }}:</strong>
|
<strong>{{ "total" | i18n }}:</strong>
|
||||||
{{ formGroup.value.seatCount || 0 }} × {{ options.seatPrice | currency : "$" }} =
|
{{ formGroup.value.seatCount || 0 }} × {{ options.seatPrice | currency : "$" }} =
|
||||||
{{ seatTotal | currency : "$" }} / {{ options.interval | i18n }}
|
{{ seatTotalCost | currency : "$" }} / {{ options.interval | i18n }}
|
||||||
</bit-hint>
|
</bit-hint>
|
||||||
</bit-form-field>
|
</bit-form-field>
|
||||||
<bit-form-control>
|
<bit-form-control>
|
||||||
@ -28,7 +28,7 @@
|
|||||||
<bit-hint>
|
<bit-hint>
|
||||||
<strong>{{ "maxSeatCost" | i18n }}:</strong>
|
<strong>{{ "maxSeatCost" | i18n }}:</strong>
|
||||||
{{ formGroup.value.maxAutoscaleSeats || 0 }} ×
|
{{ formGroup.value.maxAutoscaleSeats || 0 }} ×
|
||||||
{{ options.seatPrice | currency : "$" }} = {{ maxSeatTotal | currency : "$" }} /
|
{{ options.seatPrice | currency : "$" }} = {{ maxSeatTotalCost | currency : "$" }} /
|
||||||
{{ options.interval | i18n }}
|
{{ options.interval | i18n }}
|
||||||
</bit-hint>
|
</bit-hint>
|
||||||
</bit-form-field>
|
</bit-form-field>
|
||||||
@ -44,16 +44,14 @@
|
|||||||
/>
|
/>
|
||||||
<bit-hint>
|
<bit-hint>
|
||||||
<div>
|
<div>
|
||||||
{{
|
{{ "includedServiceAccounts" | i18n : options.baseServiceAccountCount }}
|
||||||
"additionalServiceAccountsDesc"
|
{{ "addAdditionalServiceAccounts" | i18n : (monthlyServiceAccountPrice | currency : "$") }}
|
||||||
| i18n : options.baseServiceAccountCount : (monthlyServiceAccountPrice | currency : "$")
|
|
||||||
}}
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<strong>{{ "total" | i18n }}:</strong>
|
<strong>{{ "total" | i18n }}:</strong>
|
||||||
{{ formGroup.value.additionalServiceAccounts || 0 }} ×
|
{{ formGroup.value.additionalServiceAccounts || 0 }} ×
|
||||||
{{ options.additionalServiceAccountPrice | currency : "$" }} =
|
{{ options.additionalServiceAccountPrice | currency : "$" }} =
|
||||||
{{ serviceAccountTotal | currency : "$" }} / {{ options.interval | i18n }}
|
{{ serviceAccountTotalCost | currency : "$" }} / {{ options.interval | i18n }}
|
||||||
</div>
|
</div>
|
||||||
</bit-hint>
|
</bit-hint>
|
||||||
</bit-form-field>
|
</bit-form-field>
|
||||||
@ -80,10 +78,13 @@
|
|||||||
[min]="formGroup.value.additionalServiceAccounts"
|
[min]="formGroup.value.additionalServiceAccounts"
|
||||||
/>
|
/>
|
||||||
<bit-hint>
|
<bit-hint>
|
||||||
|
<div>
|
||||||
|
{{ "includedServiceAccounts" | i18n : options.baseServiceAccountCount }}
|
||||||
|
</div>
|
||||||
<strong>{{ "maxServiceAccountCost" | i18n }}:</strong>
|
<strong>{{ "maxServiceAccountCost" | i18n }}:</strong>
|
||||||
{{ formGroup.value.maxAutoscaleServiceAccounts || 0 }} ×
|
{{ maxAdditionalServiceAccounts }} ×
|
||||||
{{ options.additionalServiceAccountPrice | currency : "$" }} =
|
{{ options.additionalServiceAccountPrice | currency : "$" }} =
|
||||||
{{ maxServiceAccountTotal | currency : "$" }} / {{ options.interval | i18n }}
|
{{ maxServiceAccountTotalCost | currency : "$" }} / {{ options.interval | i18n }}
|
||||||
</bit-hint>
|
</bit-hint>
|
||||||
</bit-form-field>
|
</bit-form-field>
|
||||||
<button type="submit" bitButton buttonType="primary" bitFormButton>
|
<button type="submit" bitButton buttonType="primary" bitFormButton>
|
||||||
|
@ -72,24 +72,26 @@ export class SecretsManagerAdjustSubscriptionComponent implements OnInit, OnDest
|
|||||||
: this.options.additionalServiceAccountPrice / 12;
|
: this.options.additionalServiceAccountPrice / 12;
|
||||||
}
|
}
|
||||||
|
|
||||||
get serviceAccountTotal(): number {
|
get serviceAccountTotalCost(): number {
|
||||||
return Math.abs(
|
return Math.abs(
|
||||||
this.formGroup.value.additionalServiceAccounts * this.options.additionalServiceAccountPrice
|
this.formGroup.value.additionalServiceAccounts * this.options.additionalServiceAccountPrice
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
get seatTotal(): number {
|
get seatTotalCost(): number {
|
||||||
return Math.abs(this.formGroup.value.seatCount * this.options.seatPrice);
|
return Math.abs(this.formGroup.value.seatCount * this.options.seatPrice);
|
||||||
}
|
}
|
||||||
|
|
||||||
get maxServiceAccountTotal(): number {
|
get maxAdditionalServiceAccounts(): number {
|
||||||
return Math.abs(
|
const maxTotalServiceAccounts = this.formGroup.value.maxAutoscaleServiceAccounts ?? 0;
|
||||||
(this.formGroup.value.maxAutoscaleServiceAccounts ?? 0) *
|
return Math.max(0, maxTotalServiceAccounts - this.options.baseServiceAccountCount);
|
||||||
this.options.additionalServiceAccountPrice
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get maxSeatTotal(): number {
|
get maxServiceAccountTotalCost(): number {
|
||||||
|
return this.maxAdditionalServiceAccounts * this.options.additionalServiceAccountPrice;
|
||||||
|
}
|
||||||
|
|
||||||
|
get maxSeatTotalCost(): number {
|
||||||
return Math.abs((this.formGroup.value.maxAutoscaleSeats ?? 0) * this.options.seatPrice);
|
return Math.abs((this.formGroup.value.maxAutoscaleSeats ?? 0) * this.options.seatPrice);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -115,7 +117,7 @@ export class SecretsManagerAdjustSubscriptionComponent implements OnInit, OnDest
|
|||||||
|
|
||||||
if (value.limitServiceAccounts) {
|
if (value.limitServiceAccounts) {
|
||||||
maxAutoscaleServiceAccountsControl.setValidators([
|
maxAutoscaleServiceAccountsControl.setValidators([
|
||||||
Validators.min(value.additionalServiceAccounts),
|
Validators.min(value.additionalServiceAccounts + this.options.baseServiceAccountCount),
|
||||||
]);
|
]);
|
||||||
maxAutoscaleServiceAccountsControl.enable({ emitEvent: false });
|
maxAutoscaleServiceAccountsControl.enable({ emitEvent: false });
|
||||||
} else {
|
} else {
|
||||||
|
@ -2,6 +2,8 @@ import { Component, EventEmitter, Input, Output } from "@angular/core";
|
|||||||
import { FormBuilder } from "@angular/forms";
|
import { FormBuilder } from "@angular/forms";
|
||||||
|
|
||||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||||
|
import { InternalOrganizationServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||||
|
import { OrganizationData } from "@bitwarden/common/admin-console/models/data/organization.data";
|
||||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||||
import { SecretsManagerSubscribeRequest } from "@bitwarden/common/billing/models/request/sm-subscribe.request";
|
import { SecretsManagerSubscribeRequest } from "@bitwarden/common/billing/models/request/sm-subscribe.request";
|
||||||
import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response";
|
import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response";
|
||||||
@ -25,7 +27,8 @@ export class SecretsManagerSubscribeStandaloneComponent {
|
|||||||
private formBuilder: FormBuilder,
|
private formBuilder: FormBuilder,
|
||||||
private platformUtilsService: PlatformUtilsService,
|
private platformUtilsService: PlatformUtilsService,
|
||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
private organizationApiService: OrganizationApiServiceAbstraction
|
private organizationApiService: OrganizationApiServiceAbstraction,
|
||||||
|
private organizationService: InternalOrganizationServiceAbstraction
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
submit = async () => {
|
submit = async () => {
|
||||||
@ -37,7 +40,15 @@ export class SecretsManagerSubscribeStandaloneComponent {
|
|||||||
? this.formGroup.value.additionalServiceAccounts
|
? this.formGroup.value.additionalServiceAccounts
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
await this.organizationApiService.subscribeToSecretsManager(this.organization.id, request);
|
const profileOrganization = await this.organizationApiService.subscribeToSecretsManager(
|
||||||
|
this.organization.id,
|
||||||
|
request
|
||||||
|
);
|
||||||
|
const organizationData = new OrganizationData(profileOrganization, {
|
||||||
|
isMember: this.organization.isMember,
|
||||||
|
isProviderUser: this.organization.isProviderUser,
|
||||||
|
});
|
||||||
|
await this.organizationService.upsert(organizationData);
|
||||||
|
|
||||||
this.platformUtilsService.showToast("success", null, this.i18nService.t("subscriptionUpdated"));
|
this.platformUtilsService.showToast("success", null, this.i18nService.t("subscriptionUpdated"));
|
||||||
|
|
||||||
|
@ -56,10 +56,13 @@
|
|||||||
<bit-form-field>
|
<bit-form-field>
|
||||||
<bit-label>{{ "additionalServiceAccounts" | i18n }}</bit-label>
|
<bit-label>{{ "additionalServiceAccounts" | i18n }}</bit-label>
|
||||||
<input bitInput formControlName="additionalServiceAccounts" type="number" />
|
<input bitInput formControlName="additionalServiceAccounts" type="number" />
|
||||||
<bit-hint>{{
|
<bit-hint>
|
||||||
"additionalServiceAccountsDesc"
|
{{ "includedServiceAccounts" | i18n : serviceAccountsIncluded }}
|
||||||
| i18n : serviceAccountsIncluded : (monthlyCostPerServiceAccount | currency : "$")
|
{{
|
||||||
}}</bit-hint>
|
"addAdditionalServiceAccounts"
|
||||||
|
| i18n : (monthlyCostPerServiceAccount | currency : "$")
|
||||||
|
}}
|
||||||
|
</bit-hint>
|
||||||
</bit-form-field>
|
</bit-form-field>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -7027,15 +7027,20 @@
|
|||||||
"additionalServiceAccounts": {
|
"additionalServiceAccounts": {
|
||||||
"message": "Additional service accounts"
|
"message": "Additional service accounts"
|
||||||
},
|
},
|
||||||
"additionalServiceAccountsDesc": {
|
"includedServiceAccounts": {
|
||||||
"message": "Your plan comes with $COUNT$ service accounts. You can add additional service accounts for $COST$ per month.",
|
"message": "Your plan comes with $COUNT$ service accounts.",
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"count": {
|
"count": {
|
||||||
"content": "$1",
|
"content": "$1",
|
||||||
"example": "50"
|
"example": "50"
|
||||||
},
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"addAdditionalServiceAccounts": {
|
||||||
|
"message": "You can add additional service accounts for $COST$ per month.",
|
||||||
|
"placeholders": {
|
||||||
"cost": {
|
"cost": {
|
||||||
"content": "$2",
|
"content": "$1",
|
||||||
"example": "$0.50"
|
"example": "$0.50"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -7063,5 +7068,24 @@
|
|||||||
},
|
},
|
||||||
"maxServiceAccountCost": {
|
"maxServiceAccountCost": {
|
||||||
"message": "Max potential service account cost"
|
"message": "Max potential service account cost"
|
||||||
|
},
|
||||||
|
"smBetaEndedDesc": {
|
||||||
|
"message": "The Secrets Manager Beta ended $BETA_ENDING_DATE$. You have $DAYS$ days left to add Secrets Manager to your paid subscription and maintain access to Secrets Manager data. Contact Customer Success to add Secrets Manager to your subscription.",
|
||||||
|
"placeholders": {
|
||||||
|
"beta_ending_date": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "August 1, 2023"
|
||||||
|
},
|
||||||
|
"days": {
|
||||||
|
"content": "$2",
|
||||||
|
"example": "11"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"betaEnding": {
|
||||||
|
"message": "Beta Ending"
|
||||||
|
},
|
||||||
|
"beta": {
|
||||||
|
"message": "Beta"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -22,7 +22,7 @@ import { VaultTimeoutService as VaultTimeoutServiceAbstraction } from "@bitwarde
|
|||||||
import { VaultTimeoutSettingsService as VaultTimeoutSettingsServiceAbstraction } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeoutSettings.service";
|
import { VaultTimeoutSettingsService as VaultTimeoutSettingsServiceAbstraction } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeoutSettings.service";
|
||||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||||
import {
|
import {
|
||||||
InternalOrganizationService,
|
InternalOrganizationServiceAbstraction,
|
||||||
OrganizationService as OrganizationServiceAbstraction,
|
OrganizationService as OrganizationServiceAbstraction,
|
||||||
} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||||
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
|
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
|
||||||
@ -583,7 +583,7 @@ import { AbstractThemingService } from "./theming/theming.service.abstraction";
|
|||||||
deps: [StateServiceAbstraction],
|
deps: [StateServiceAbstraction],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: InternalOrganizationService,
|
provide: InternalOrganizationServiceAbstraction,
|
||||||
useExisting: OrganizationServiceAbstraction,
|
useExisting: OrganizationServiceAbstraction,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -210,7 +210,7 @@ export abstract class OrganizationUserService {
|
|||||||
abstract putOrganizationUserBulkEnableSecretsManager(
|
abstract putOrganizationUserBulkEnableSecretsManager(
|
||||||
organizationId: string,
|
organizationId: string,
|
||||||
ids: string[]
|
ids: string[]
|
||||||
): Promise<ListResponse<OrganizationUserBulkResponse>>;
|
): Promise<void>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete an organization user
|
* Delete an organization user
|
||||||
|
@ -26,6 +26,7 @@ import { OrganizationApiKeyInformationResponse } from "../../models/response/org
|
|||||||
import { OrganizationAutoEnrollStatusResponse } from "../../models/response/organization-auto-enroll-status.response";
|
import { OrganizationAutoEnrollStatusResponse } from "../../models/response/organization-auto-enroll-status.response";
|
||||||
import { OrganizationKeysResponse } from "../../models/response/organization-keys.response";
|
import { OrganizationKeysResponse } from "../../models/response/organization-keys.response";
|
||||||
import { OrganizationResponse } from "../../models/response/organization.response";
|
import { OrganizationResponse } from "../../models/response/organization.response";
|
||||||
|
import { ProfileOrganizationResponse } from "../../models/response/profile-organization.response";
|
||||||
|
|
||||||
export class OrganizationApiServiceAbstraction {
|
export class OrganizationApiServiceAbstraction {
|
||||||
get: (id: string) => Promise<OrganizationResponse>;
|
get: (id: string) => Promise<OrganizationResponse>;
|
||||||
@ -68,5 +69,8 @@ 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>;
|
||||||
subscribeToSecretsManager: (id: string, request: SecretsManagerSubscribeRequest) => Promise<void>;
|
subscribeToSecretsManager: (
|
||||||
|
id: string,
|
||||||
|
request: SecretsManagerSubscribeRequest
|
||||||
|
) => Promise<ProfileOrganizationResponse>;
|
||||||
}
|
}
|
||||||
|
@ -86,6 +86,7 @@ export abstract class OrganizationService {
|
|||||||
hasOrganizations: () => boolean;
|
hasOrganizations: () => boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export abstract class InternalOrganizationService extends OrganizationService {
|
export abstract class InternalOrganizationServiceAbstraction extends OrganizationService {
|
||||||
replace: (organizations: { [id: string]: OrganizationData }) => Promise<void>;
|
replace: (organizations: { [id: string]: OrganizationData }) => Promise<void>;
|
||||||
|
upsert: (OrganizationData: OrganizationData | OrganizationData[]) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
@ -29,6 +29,7 @@ import { OrganizationApiKeyInformationResponse } from "../../models/response/org
|
|||||||
import { OrganizationAutoEnrollStatusResponse } from "../../models/response/organization-auto-enroll-status.response";
|
import { OrganizationAutoEnrollStatusResponse } from "../../models/response/organization-auto-enroll-status.response";
|
||||||
import { OrganizationKeysResponse } from "../../models/response/organization-keys.response";
|
import { OrganizationKeysResponse } from "../../models/response/organization-keys.response";
|
||||||
import { OrganizationResponse } from "../../models/response/organization.response";
|
import { OrganizationResponse } from "../../models/response/organization.response";
|
||||||
|
import { ProfileOrganizationResponse } from "../../models/response/profile-organization.response";
|
||||||
|
|
||||||
export class OrganizationApiService implements OrganizationApiServiceAbstraction {
|
export class OrganizationApiService implements OrganizationApiServiceAbstraction {
|
||||||
constructor(private apiService: ApiService, private syncService: SyncService) {}
|
constructor(private apiService: ApiService, private syncService: SyncService) {}
|
||||||
@ -311,13 +312,14 @@ export class OrganizationApiService implements OrganizationApiServiceAbstraction
|
|||||||
async subscribeToSecretsManager(
|
async subscribeToSecretsManager(
|
||||||
id: string,
|
id: string,
|
||||||
request: SecretsManagerSubscribeRequest
|
request: SecretsManagerSubscribeRequest
|
||||||
): Promise<void> {
|
): Promise<ProfileOrganizationResponse> {
|
||||||
return await this.apiService.send(
|
const r = await this.apiService.send(
|
||||||
"POST",
|
"POST",
|
||||||
"/organizations/" + id + "/subscribe-secrets-manager",
|
"/organizations/" + id + "/subscribe-secrets-manager",
|
||||||
request,
|
request,
|
||||||
true,
|
true,
|
||||||
false
|
true
|
||||||
);
|
);
|
||||||
|
return new ProfileOrganizationResponse(r);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@ import { BehaviorSubject, concatMap, map, Observable } from "rxjs";
|
|||||||
|
|
||||||
import { StateService } from "../../../platform/abstractions/state.service";
|
import { StateService } from "../../../platform/abstractions/state.service";
|
||||||
import {
|
import {
|
||||||
InternalOrganizationService as InternalOrganizationServiceAbstraction,
|
InternalOrganizationServiceAbstraction,
|
||||||
isMember,
|
isMember,
|
||||||
} from "../../abstractions/organization/organization.service.abstraction";
|
} from "../../abstractions/organization/organization.service.abstraction";
|
||||||
import { OrganizationData } from "../../models/data/organization.data";
|
import { OrganizationData } from "../../models/data/organization.data";
|
||||||
|
@ -12,6 +12,7 @@ export class OrganizationSubscriptionResponse extends OrganizationResponse {
|
|||||||
upcomingInvoice: BillingSubscriptionUpcomingInvoiceResponse;
|
upcomingInvoice: BillingSubscriptionUpcomingInvoiceResponse;
|
||||||
expiration: string;
|
expiration: string;
|
||||||
expirationWithoutGracePeriod: string;
|
expirationWithoutGracePeriod: string;
|
||||||
|
secretsManagerBeta: boolean;
|
||||||
|
|
||||||
constructor(response: any) {
|
constructor(response: any) {
|
||||||
super(response);
|
super(response);
|
||||||
@ -26,5 +27,6 @@ export class OrganizationSubscriptionResponse extends OrganizationResponse {
|
|||||||
: new BillingSubscriptionUpcomingInvoiceResponse(upcomingInvoice);
|
: new BillingSubscriptionUpcomingInvoiceResponse(upcomingInvoice);
|
||||||
this.expiration = this.getResponseProperty("Expiration");
|
this.expiration = this.getResponseProperty("Expiration");
|
||||||
this.expirationWithoutGracePeriod = this.getResponseProperty("ExpirationWithoutGracePeriod");
|
this.expirationWithoutGracePeriod = this.getResponseProperty("ExpirationWithoutGracePeriod");
|
||||||
|
this.secretsManagerBeta = this.getResponseProperty("SecretsManagerBeta");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -358,4 +358,32 @@ describe("Utils Service", () => {
|
|||||||
expect(actual.protocol).toBe("http:");
|
expect(actual.protocol).toBe("http:");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("daysRemaining", () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
const now = new Date(2023, 9, 2, 10);
|
||||||
|
jest.spyOn(Date, "now").mockReturnValue(now.getTime());
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return 0 for equal dates", () => {
|
||||||
|
expect(Utils.daysRemaining(new Date(2023, 9, 2))).toBe(0);
|
||||||
|
expect(Utils.daysRemaining(new Date(2023, 9, 2, 12))).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return 0 for dates in the past", () => {
|
||||||
|
expect(Utils.daysRemaining(new Date(2020, 5, 11))).toBe(0);
|
||||||
|
expect(Utils.daysRemaining(new Date(2023, 9, 1))).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle future dates", () => {
|
||||||
|
expect(Utils.daysRemaining(new Date(2023, 9, 3, 10))).toBe(1);
|
||||||
|
expect(Utils.daysRemaining(new Date(2023, 10, 12, 10))).toBe(41);
|
||||||
|
// leap year
|
||||||
|
expect(Utils.daysRemaining(new Date(2024, 9, 2, 10))).toBe(366);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -538,6 +538,16 @@ export class Utils {
|
|||||||
return of(undefined).pipe(switchMap(() => generator()));
|
return of(undefined).pipe(switchMap(() => generator()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the number of days remaining before a target date arrives.
|
||||||
|
* Returns 0 if the day has already passed.
|
||||||
|
*/
|
||||||
|
static daysRemaining(targetDate: Date): number {
|
||||||
|
const diffTime = targetDate.getTime() - Date.now();
|
||||||
|
const msPerDay = 86400000;
|
||||||
|
return Math.max(0, Math.floor(diffTime / msPerDay));
|
||||||
|
}
|
||||||
|
|
||||||
private static isAppleMobile(win: Window) {
|
private static isAppleMobile(win: Window) {
|
||||||
return (
|
return (
|
||||||
win.navigator.userAgent.match(/iPhone/i) != null ||
|
win.navigator.userAgent.match(/iPhone/i) != null ||
|
||||||
|
@ -209,15 +209,14 @@ export class OrganizationUserServiceImplementation implements OrganizationUserSe
|
|||||||
async putOrganizationUserBulkEnableSecretsManager(
|
async putOrganizationUserBulkEnableSecretsManager(
|
||||||
organizationId: string,
|
organizationId: string,
|
||||||
ids: string[]
|
ids: string[]
|
||||||
): Promise<ListResponse<OrganizationUserBulkResponse>> {
|
): Promise<void> {
|
||||||
const r = await this.apiService.send(
|
await this.apiService.send(
|
||||||
"PUT",
|
"PUT",
|
||||||
"/organizations/" + organizationId + "/users/enable-secrets-manager",
|
"/organizations/" + organizationId + "/users/enable-secrets-manager",
|
||||||
new OrganizationUserBulkRequest(ids),
|
new OrganizationUserBulkRequest(ids),
|
||||||
true,
|
true,
|
||||||
true
|
false
|
||||||
);
|
);
|
||||||
return new ListResponse(r, OrganizationUserBulkResponse);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
putOrganizationUser(
|
putOrganizationUser(
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { ApiService } from "../../../abstractions/api.service";
|
import { ApiService } from "../../../abstractions/api.service";
|
||||||
import { SettingsService } from "../../../abstractions/settings.service";
|
import { SettingsService } from "../../../abstractions/settings.service";
|
||||||
import { InternalOrganizationService } from "../../../admin-console/abstractions/organization/organization.service.abstraction";
|
import { InternalOrganizationServiceAbstraction } from "../../../admin-console/abstractions/organization/organization.service.abstraction";
|
||||||
import { InternalPolicyService } from "../../../admin-console/abstractions/policy/policy.service.abstraction";
|
import { InternalPolicyService } from "../../../admin-console/abstractions/policy/policy.service.abstraction";
|
||||||
import { ProviderService } from "../../../admin-console/abstractions/provider.service";
|
import { ProviderService } from "../../../admin-console/abstractions/provider.service";
|
||||||
import { OrganizationData } from "../../../admin-console/models/data/organization.data";
|
import { OrganizationData } from "../../../admin-console/models/data/organization.data";
|
||||||
@ -55,7 +55,7 @@ export class SyncService implements SyncServiceAbstraction {
|
|||||||
private stateService: StateService,
|
private stateService: StateService,
|
||||||
private providerService: ProviderService,
|
private providerService: ProviderService,
|
||||||
private folderApiService: FolderApiServiceAbstraction,
|
private folderApiService: FolderApiServiceAbstraction,
|
||||||
private organizationService: InternalOrganizationService,
|
private organizationService: InternalOrganizationServiceAbstraction,
|
||||||
private sendApiService: SendApiService,
|
private sendApiService: SendApiService,
|
||||||
private logoutCallback: (expired: boolean) => Promise<void>
|
private logoutCallback: (expired: boolean) => Promise<void>
|
||||||
) {}
|
) {}
|
||||||
|
Loading…
Reference in New Issue
Block a user