[AC-2708] Upgrading from a password manager only subscription (#10320)
* Add changes for the upgrade dialog * Resolve the free org to any org type besides Families * Resolve the pr comments on navigation * resolve family plan upgrade from free
This commit is contained in:
parent
8c78959aaf
commit
6896ef2392
|
@ -33,7 +33,9 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga
|
||||||
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||||
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 { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billilng-api.service.abstraction";
|
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billilng-api.service.abstraction";
|
||||||
import { ProductTierType } from "@bitwarden/common/billing/enums";
|
import { isNotSelfUpgradable, ProductTierType } from "@bitwarden/common/billing/enums";
|
||||||
|
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||||
|
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||||
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";
|
||||||
|
@ -45,6 +47,10 @@ import { Collection } from "@bitwarden/common/vault/models/domain/collection";
|
||||||
import { CollectionDetailsResponse } from "@bitwarden/common/vault/models/response/collection.response";
|
import { CollectionDetailsResponse } from "@bitwarden/common/vault/models/response/collection.response";
|
||||||
import { DialogService, SimpleDialogOptions, ToastService } from "@bitwarden/components";
|
import { DialogService, SimpleDialogOptions, ToastService } from "@bitwarden/components";
|
||||||
|
|
||||||
|
import {
|
||||||
|
ChangePlanDialogResultType,
|
||||||
|
openChangePlanDialog,
|
||||||
|
} from "../../../billing/organizations/change-plan-dialog.component";
|
||||||
import { BaseMembersComponent } from "../../common/base-members.component";
|
import { BaseMembersComponent } from "../../common/base-members.component";
|
||||||
import { PeopleTableDataSource } from "../../common/people-table-data-source";
|
import { PeopleTableDataSource } from "../../common/people-table-data-source";
|
||||||
import { GroupService } from "../core";
|
import { GroupService } from "../core";
|
||||||
|
@ -86,6 +92,10 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
||||||
|
|
||||||
protected canUseSecretsManager$: Observable<boolean>;
|
protected canUseSecretsManager$: Observable<boolean>;
|
||||||
|
|
||||||
|
protected EnableUpgradePasswordManagerSub$ = this.configService.getFeatureFlag$(
|
||||||
|
FeatureFlag.EnableUpgradePasswordManagerSub,
|
||||||
|
);
|
||||||
|
|
||||||
// Fixed sizes used for cdkVirtualScroll
|
// Fixed sizes used for cdkVirtualScroll
|
||||||
protected rowHeight = 62;
|
protected rowHeight = 62;
|
||||||
protected rowHeightClass = `tw-h-[62px]`;
|
protected rowHeightClass = `tw-h-[62px]`;
|
||||||
|
@ -112,6 +122,7 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
||||||
private collectionService: CollectionService,
|
private collectionService: CollectionService,
|
||||||
private billingApiService: BillingApiServiceAbstraction,
|
private billingApiService: BillingApiServiceAbstraction,
|
||||||
private modalService: ModalService,
|
private modalService: ModalService,
|
||||||
|
private configService: ConfigService,
|
||||||
) {
|
) {
|
||||||
super(
|
super(
|
||||||
apiService,
|
apiService,
|
||||||
|
@ -375,6 +386,9 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
||||||
case ProductTierType.TeamsStarter:
|
case ProductTierType.TeamsStarter:
|
||||||
product = "teamsStarterPlan";
|
product = "teamsStarterPlan";
|
||||||
break;
|
break;
|
||||||
|
case ProductTierType.Families:
|
||||||
|
product = "familiesPlan";
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unsupported product type: ${productType}`);
|
throw new Error(`Unsupported product type: ${productType}`);
|
||||||
}
|
}
|
||||||
|
@ -395,7 +409,7 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
||||||
|
|
||||||
const productType = this.organization.productTierType;
|
const productType = this.organization.productTierType;
|
||||||
|
|
||||||
if (productType !== ProductTierType.Free && productType !== ProductTierType.TeamsStarter) {
|
if (isNotSelfUpgradable(productType)) {
|
||||||
throw new Error(`Unsupported product type: ${productType}`);
|
throw new Error(`Unsupported product type: ${productType}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -409,7 +423,7 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
||||||
|
|
||||||
const productType = this.organization.productTierType;
|
const productType = this.organization.productTierType;
|
||||||
|
|
||||||
if (productType !== ProductTierType.Free && productType !== ProductTierType.TeamsStarter) {
|
if (isNotSelfUpgradable(productType)) {
|
||||||
throw new Error(`Unsupported product type: ${this.organization.productTierType}`);
|
throw new Error(`Unsupported product type: ${this.organization.productTierType}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -459,11 +473,32 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
||||||
!user &&
|
!user &&
|
||||||
this.dataSource.data.length === this.organization.seats &&
|
this.dataSource.data.length === this.organization.seats &&
|
||||||
(this.organization.productTierType === ProductTierType.Free ||
|
(this.organization.productTierType === ProductTierType.Free ||
|
||||||
this.organization.productTierType === ProductTierType.TeamsStarter)
|
this.organization.productTierType === ProductTierType.TeamsStarter ||
|
||||||
|
this.organization.productTierType === ProductTierType.Families)
|
||||||
) {
|
) {
|
||||||
// Show org upgrade modal
|
const EnableUpgradePasswordManagerSub = await firstValueFrom(
|
||||||
await this.showSeatLimitReachedDialog();
|
this.EnableUpgradePasswordManagerSub$,
|
||||||
return;
|
);
|
||||||
|
if (EnableUpgradePasswordManagerSub) {
|
||||||
|
const reference = openChangePlanDialog(this.dialogService, {
|
||||||
|
data: {
|
||||||
|
organizationId: this.organization.id,
|
||||||
|
subscription: null,
|
||||||
|
productTierType: this.organization.productTierType,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await lastValueFrom(reference.closed);
|
||||||
|
|
||||||
|
if (result === ChangePlanDialogResultType.Submitted) {
|
||||||
|
await this.load();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
// Show org upgrade modal
|
||||||
|
await this.showSeatLimitReachedDialog();
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const dialog = openUserAddEditDialog(this.dialogService, {
|
const dialog = openUserAddEditDialog(this.dialogService, {
|
||||||
|
|
|
@ -0,0 +1,383 @@
|
||||||
|
<form [formGroup]="formGroup" [bitSubmit]="submit">
|
||||||
|
<bit-dialog dialogSize="large" [loading]="loading">
|
||||||
|
<span bitDialogTitle class="tw-font-semibold">
|
||||||
|
{{ "upgradeFreeOrganization" | i18n: currentPlanName }}
|
||||||
|
</span>
|
||||||
|
<div bitDialogContent>
|
||||||
|
<p>{{ "upgradePlan" | i18n }}</p>
|
||||||
|
<div class="tw-mb-3">
|
||||||
|
<span class="tw-text-lg tw-pr-1 tw-font-bold">{{ "selectAPlan" | i18n }}</span>
|
||||||
|
<bit-radio-group
|
||||||
|
formControlName="planInterval"
|
||||||
|
class="tw-flex tw-items-start tw-gap-5"
|
||||||
|
(change)="planTypeChanged()"
|
||||||
|
>
|
||||||
|
<bit-radio-button
|
||||||
|
class="tw-inline-block"
|
||||||
|
*ngFor="let planInterval of getPlanIntervals()"
|
||||||
|
id="plan-annually"
|
||||||
|
[value]="planInterval.value"
|
||||||
|
>
|
||||||
|
<bit-label>
|
||||||
|
<i class="bwi" aria-hidden="true"></i>
|
||||||
|
{{ planInterval.name }} <span
|
||||||
|
*ngIf="
|
||||||
|
this.discountPercentageFromSub > 0
|
||||||
|
? discountPercentageFromSub
|
||||||
|
: this.discountPercentage && planInterval.value === 1
|
||||||
|
"
|
||||||
|
bitBadge
|
||||||
|
variant="success"
|
||||||
|
>{{
|
||||||
|
"upgradeDiscount"
|
||||||
|
| i18n
|
||||||
|
: (this.discountPercentageFromSub > 0
|
||||||
|
? discountPercentageFromSub
|
||||||
|
: this.discountPercentage)
|
||||||
|
}}</span
|
||||||
|
></bit-label
|
||||||
|
>
|
||||||
|
</bit-radio-button>
|
||||||
|
</bit-radio-group>
|
||||||
|
</div>
|
||||||
|
<ng-container *ngIf="!loading && !selfHosted && this.passwordManagerPlans">
|
||||||
|
<div
|
||||||
|
class="tw-grid tw-grid-flow-col tw-gap-4 tw-mb-4"
|
||||||
|
[class]="'tw-grid-cols-' + selectableProducts.length"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
*ngFor="let selectableProduct of selectableProducts; let i = index"
|
||||||
|
[ngClass]="getPlanCardContainerClasses(selectableProduct, i)"
|
||||||
|
(click)="selectPlan(selectableProduct)"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<div class="tw-relative">
|
||||||
|
<div
|
||||||
|
*ngIf="selectableProduct == selectedPlan"
|
||||||
|
class="tw-bg-primary-600 tw-text-center !tw-text-contrast tw-text-sm tw-font-bold tw-py-1 group-hover:tw-bg-primary-700"
|
||||||
|
>
|
||||||
|
{{ "selected" | i18n }}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="tw-px-2 tw-py-4"
|
||||||
|
[ngClass]="{ 'tw-pt-10': !(selectableProduct == selectedPlan) }"
|
||||||
|
>
|
||||||
|
<h3 class="tw-text-lg tw-font-bold tw-uppercase">
|
||||||
|
{{ selectableProduct.nameLocalizationKey | i18n }}
|
||||||
|
</h3>
|
||||||
|
<span *ngIf="selectableProduct.productTier != productTypes.Free">
|
||||||
|
<ng-container
|
||||||
|
*ngIf="selectableProduct.PasswordManager.basePrice && !acceptingSponsorship"
|
||||||
|
>
|
||||||
|
<b class="tw-text-lg tw-font-semibold">
|
||||||
|
{{
|
||||||
|
(selectableProduct.isAnnual
|
||||||
|
? selectableProduct.PasswordManager.basePrice / 12
|
||||||
|
: selectableProduct.PasswordManager.basePrice
|
||||||
|
) | currency: "$"
|
||||||
|
}}
|
||||||
|
</b>
|
||||||
|
<b class="tw-text-sm tw-font-semibold">
|
||||||
|
/{{ "month" | i18n }}
|
||||||
|
{{ "includesXMembers" | i18n: selectableProduct.PasswordManager.baseSeats }}
|
||||||
|
<ng-container
|
||||||
|
*ngIf="selectableProduct.PasswordManager.hasAdditionalSeatsOption"
|
||||||
|
>
|
||||||
|
{{ ("additionalUsers" | i18n).toLowerCase() }}
|
||||||
|
{{
|
||||||
|
(selectableProduct.isAnnual
|
||||||
|
? selectableProduct.PasswordManager.seatPrice / 12
|
||||||
|
: selectableProduct.PasswordManager.seatPrice
|
||||||
|
) | currency: "$"
|
||||||
|
}}
|
||||||
|
/{{ "month" | i18n }}
|
||||||
|
</ng-container>
|
||||||
|
</b>
|
||||||
|
</ng-container>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
*ngIf="
|
||||||
|
!selectableProduct.PasswordManager.basePrice &&
|
||||||
|
selectableProduct.PasswordManager.hasAdditionalSeatsOption
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<b class="tw-text-lg tw-font-semibold"
|
||||||
|
>{{
|
||||||
|
"costPerMember"
|
||||||
|
| i18n
|
||||||
|
: ((selectableProduct.isAnnual
|
||||||
|
? selectableProduct.PasswordManager.seatPrice / 12
|
||||||
|
: selectableProduct.PasswordManager.seatPrice
|
||||||
|
)
|
||||||
|
| currency: "$")
|
||||||
|
}}
|
||||||
|
</b>
|
||||||
|
<b class="tw-text-sm tw-font-semibold"> /{{ "monthPerMember" | i18n }}</b>
|
||||||
|
</span>
|
||||||
|
<span *ngIf="selectableProduct.productTier == productTypes.Free"
|
||||||
|
>{{ "freeForever" | i18n }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ng-container
|
||||||
|
*ngIf="
|
||||||
|
selectableProduct.productTier === productTypes.Enterprise;
|
||||||
|
else nonEnterprisePlans
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<p class="tw-text-xs tw-px-2">{{ "upgradeEnterpriseMessage" | i18n }}</p>
|
||||||
|
<p class="tw-text-xs tw-px-2 tw-mb-0">{{ "includeAllTeamsFeatures" | i18n }}</p>
|
||||||
|
<ul class="tw-px-3 tw-pb-2 tw-list-inside tw-mb-0 tw-text-xs">
|
||||||
|
<li *ngIf="selectableProduct.hasPolicies">
|
||||||
|
{{ "includeEnterprisePolicies" | i18n }}
|
||||||
|
</li>
|
||||||
|
<li *ngIf="selectableProduct.hasSso">
|
||||||
|
{{ "includeSsoAuthenticationMessage" | i18n }}
|
||||||
|
</li>
|
||||||
|
<li *ngIf="selectableProduct.hasSelfHost">{{ "optionalOnPremHosting" | i18n }}</li>
|
||||||
|
</ul>
|
||||||
|
</ng-container>
|
||||||
|
<ng-template #nonEnterprisePlans>
|
||||||
|
<ng-container
|
||||||
|
*ngIf="
|
||||||
|
selectableProduct.productTier === productTypes.Teams &&
|
||||||
|
teamsStarterPlanIsAvailable;
|
||||||
|
else fullFeatureList
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<ul class="tw-px-2 tw-pb-2 tw-list-inside tw-mb-0 tw-text-xs">
|
||||||
|
<li>{{ "includeAllTeamsStarterFeatures" | i18n }}</li>
|
||||||
|
<li>{{ "chooseMonthlyOrAnnualBilling" | i18n }}</li>
|
||||||
|
<li>{{ "abilityToAddMoreThanNMembers" | i18n: 10 }}</li>
|
||||||
|
</ul>
|
||||||
|
</ng-container>
|
||||||
|
<ng-template #fullFeatureList>
|
||||||
|
<p
|
||||||
|
*ngIf="selectableProduct.productTier === productTypes.Teams"
|
||||||
|
class="tw-text-xs tw-px-2"
|
||||||
|
>
|
||||||
|
{{ "upgradeTeamsMessage" | i18n }}
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
*ngIf="selectableProduct.productTier === productTypes.Families"
|
||||||
|
class="tw-text-xs tw-px-2"
|
||||||
|
>
|
||||||
|
{{ "upgradeFamilyMessage" | i18n }}
|
||||||
|
</p>
|
||||||
|
<ul class="tw-px-2 tw-pb-2 tw-list-inside tw-mb-0 tw-text-xs">
|
||||||
|
<li *ngIf="selectableProduct.productTier == productTypes.Free">
|
||||||
|
{{ "limitedUsers" | i18n: selectableProduct.PasswordManager.maxSeats }}
|
||||||
|
</li>
|
||||||
|
<li *ngIf="!selectableProduct.PasswordManager.maxSeats">
|
||||||
|
{{ "teamsInviteMessage" | i18n }}
|
||||||
|
</li>
|
||||||
|
<li *ngIf="selectableProduct.PasswordManager.maxCollections">
|
||||||
|
{{
|
||||||
|
"chooseMonthlyOrAnnualBilling"
|
||||||
|
| i18n: selectableProduct.PasswordManager.maxCollections
|
||||||
|
}}
|
||||||
|
</li>
|
||||||
|
<li *ngIf="!selectableProduct.PasswordManager.maxCollections">
|
||||||
|
{{ "createUnlimitedCollections" | i18n }}
|
||||||
|
</li>
|
||||||
|
<li *ngIf="selectableProduct.hasGroups">
|
||||||
|
{{ "accessToCreateGroups" | i18n }}
|
||||||
|
</li>
|
||||||
|
<li *ngIf="selectableProduct.hasDirectory">
|
||||||
|
{{ "syncGroupsAndUsersFromDirectory" | i18n }}
|
||||||
|
</li>
|
||||||
|
<li *ngIf="selectableProduct.productTier == productTypes.Families">
|
||||||
|
{{ "accessToPremiumFeatures" | i18n }}
|
||||||
|
</li>
|
||||||
|
<li *ngIf="selectableProduct.productTier == productTypes.Families">
|
||||||
|
{{ "priorityCustomerSupport" | i18n }}
|
||||||
|
</li>
|
||||||
|
<li *ngIf="selectableProduct.hasSelfHost">
|
||||||
|
{{ "optionalOnPremHosting" | i18n }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</ng-template>
|
||||||
|
</ng-template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<!-- Payment info -->
|
||||||
|
<ng-container *ngIf="formGroup.value.productTier !== productTypes.Free">
|
||||||
|
<h2 bitTypography="h4">{{ "paymentMethod" | i18n }}</h2>
|
||||||
|
<p *ngIf="!showPayment && billing.paymentSource">
|
||||||
|
<i class="bwi bwi-fw" [ngClass]="paymentSourceClasses"></i>
|
||||||
|
{{ billing.paymentSource.description }}
|
||||||
|
<span class="ml-2 tw-text-primary-600 tw-cursor-pointer" (click)="toggleShowPayment()">{{
|
||||||
|
"changePaymentMethod" | i18n
|
||||||
|
}}</span>
|
||||||
|
<a></a>
|
||||||
|
</p>
|
||||||
|
<app-payment
|
||||||
|
*ngIf="upgradeRequiresPaymentMethod || showPayment"
|
||||||
|
[hideCredit]="true"
|
||||||
|
></app-payment>
|
||||||
|
<app-tax-info
|
||||||
|
*ngIf="showPayment || upgradeRequiresPaymentMethod"
|
||||||
|
(onCountryChanged)="changedCountry()"
|
||||||
|
></app-tax-info>
|
||||||
|
<div id="price" class="tw-mt-4">
|
||||||
|
<p class="tw-text-lg tw-mb-1">
|
||||||
|
<span class="tw-font-semibold"
|
||||||
|
>{{ "total" | i18n }}: {{ total | currency: "USD" : "$" }} USD</span
|
||||||
|
>
|
||||||
|
<span class="tw-text-xs tw-font-light"> / {{ selectedPlanInterval | i18n }}</span>
|
||||||
|
<button
|
||||||
|
(click)="toggleTotalOpened()"
|
||||||
|
type="button"
|
||||||
|
[bitIconButton]="totalOpened ? 'bwi-angle-down' : 'bwi-angle-up'"
|
||||||
|
size="small"
|
||||||
|
aria-hidden="true"
|
||||||
|
></button>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div *ngIf="totalOpened" class="row">
|
||||||
|
<bit-hint class="col-6" *ngIf="selectedInterval == planIntervals.Annually">
|
||||||
|
<p
|
||||||
|
class="tw-mb-0 tw-flex tw-justify-between"
|
||||||
|
bitTypography="body2"
|
||||||
|
*ngIf="selectedPlan.PasswordManager.basePrice"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
{{ selectedPlan.PasswordManager.baseSeats }}
|
||||||
|
{{ "members" | i18n }} ×
|
||||||
|
{{
|
||||||
|
(selectedPlan.isAnnual
|
||||||
|
? selectedPlan.PasswordManager.basePrice / 12
|
||||||
|
: selectedPlan.PasswordManager.basePrice
|
||||||
|
) | currency: "$"
|
||||||
|
}}
|
||||||
|
/{{ "year" | i18n }}
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<ng-container *ngIf="acceptingSponsorship; else notAcceptingSponsorship">
|
||||||
|
<span class="tw-line-through">{{
|
||||||
|
selectedPlan.PasswordManager.basePrice | currency: "$"
|
||||||
|
}}</span>
|
||||||
|
{{ "freeWithSponsorship" | i18n }}
|
||||||
|
</ng-container>
|
||||||
|
<ng-template #notAcceptingSponsorship>
|
||||||
|
{{ selectedPlan.PasswordManager.basePrice | currency: "$" }}
|
||||||
|
</ng-template>
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
class="tw-mb-0 tw-flex tw-justify-between"
|
||||||
|
bitTypography="body2"
|
||||||
|
*ngIf="selectedPlan.PasswordManager.hasAdditionalSeatsOption"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
<span *ngIf="selectedPlan.PasswordManager.baseSeats"
|
||||||
|
>{{ "additionalUsers" | i18n }}:</span
|
||||||
|
>
|
||||||
|
{{ organization.seats || 0 }}
|
||||||
|
<span *ngIf="!selectedPlan.PasswordManager.baseSeats">{{ "members" | i18n }}</span>
|
||||||
|
×
|
||||||
|
{{ selectedPlan.PasswordManager.seatPrice | currency: "$" }}
|
||||||
|
/{{ "year" | i18n }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span>
|
||||||
|
{{ passwordManagerSeatTotal(selectedPlan) | currency: "$" }}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
class="tw-mb-0 tw-flex tw-justify-between"
|
||||||
|
bitTypography="body2"
|
||||||
|
*ngIf="selectedPlan.PasswordManager.hasAdditionalStorageOption"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
{{ 0 }}
|
||||||
|
{{ "additionalStorageGbMessage" | i18n }}
|
||||||
|
×
|
||||||
|
{{ selectedPlan.PasswordManager.additionalStoragePricePerGb | currency: "$" }}
|
||||||
|
/{{ "year" | i18n }}
|
||||||
|
</span>
|
||||||
|
<span>{{ 0 | currency: "$" }}</span>
|
||||||
|
</p>
|
||||||
|
</bit-hint>
|
||||||
|
<bit-hint class="col-6" *ngIf="selectedInterval == planIntervals.Monthly">
|
||||||
|
<p
|
||||||
|
class="tw-mb-0 tw-flex tw-justify-between"
|
||||||
|
bitTypography="body2"
|
||||||
|
*ngIf="selectedPlan.PasswordManager.basePrice"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
{{ "basePrice" | i18n }}:
|
||||||
|
{{ selectedPlan.PasswordManager.basePrice | currency: "$" }}
|
||||||
|
{{ "monthAbbr" | i18n }}
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
{{ selectedPlan.PasswordManager.basePrice | currency: "$" }}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
class="tw-mb-0 tw-flex tw-justify-between"
|
||||||
|
bitTypography="body2"
|
||||||
|
*ngIf="selectedPlan.PasswordManager.hasAdditionalSeatsOption"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
<span *ngIf="selectedPlan.PasswordManager.baseSeats"
|
||||||
|
>{{ "additionalUsers" | i18n }}:</span
|
||||||
|
>
|
||||||
|
{{ formGroup.controls["additionalSeats"].value || 0 }}
|
||||||
|
<span *ngIf="!selectedPlan.PasswordManager.baseSeats">{{ "members" | i18n }}</span>
|
||||||
|
×
|
||||||
|
{{ selectedPlan.PasswordManager.seatPrice | currency: "$" }}
|
||||||
|
/{{ "month" | i18n }}
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
{{ passwordManagerSeatTotal(selectedPlan) | currency: "$" }}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
class="tw-mb-0 tw-flex tw-justify-between"
|
||||||
|
bitTypography="body2"
|
||||||
|
*ngIf="selectedPlan.PasswordManager.hasAdditionalStorageOption"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
{{ 0 }}
|
||||||
|
{{ "additionalStorageGbMessage" | i18n }}
|
||||||
|
×
|
||||||
|
{{ selectedPlan.PasswordManager.additionalStoragePricePerGb | currency: "$" }}
|
||||||
|
/{{ "month" | i18n }}
|
||||||
|
</span>
|
||||||
|
<span>{{ 0 | currency: "$" }}</span>
|
||||||
|
</p>
|
||||||
|
</bit-hint>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div *ngIf="totalOpened" id="price" class="row tw-mt-4">
|
||||||
|
<bit-hint class="col-6">
|
||||||
|
<p
|
||||||
|
class="tw-flex tw-justify-between tw-border-0 tw-border-solid tw-border-t tw-border-secondary-300 tw-pt-2 tw-mb-0"
|
||||||
|
>
|
||||||
|
<span class="tw-font-semibold">
|
||||||
|
{{ "total" | i18n }}
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
{{ total | currency: "USD" : "$" }}
|
||||||
|
<span class="tw-text-xs tw-font-light"> / {{ selectedPlanInterval | i18n }}</span>
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</bit-hint>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
|
<ng-container bitDialogFooter>
|
||||||
|
<button bitButton bitFormButton buttonType="primary" type="submit">
|
||||||
|
{{ "save" | i18n }}
|
||||||
|
</button>
|
||||||
|
<button bitButton buttonType="secondary" type="button" [bitDialogClose]="ResultType.Closed">
|
||||||
|
{{ "cancel" | i18n }}
|
||||||
|
</button>
|
||||||
|
</ng-container>
|
||||||
|
</bit-dialog>
|
||||||
|
</form>
|
|
@ -0,0 +1,658 @@
|
||||||
|
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
|
||||||
|
import { Component, EventEmitter, Inject, Input, OnInit, Output, ViewChild } from "@angular/core";
|
||||||
|
import { FormBuilder, Validators } from "@angular/forms";
|
||||||
|
import { Router } from "@angular/router";
|
||||||
|
import { Subject, takeUntil } from "rxjs";
|
||||||
|
|
||||||
|
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 { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||||
|
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||||
|
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 {
|
||||||
|
PaymentMethodType,
|
||||||
|
PlanType,
|
||||||
|
ProductTierType,
|
||||||
|
PlanInterval,
|
||||||
|
} from "@bitwarden/common/billing/enums";
|
||||||
|
import { PaymentRequest } from "@bitwarden/common/billing/models/request/payment.request";
|
||||||
|
import { BillingResponse } from "@bitwarden/common/billing/models/response/billing.response";
|
||||||
|
import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.response";
|
||||||
|
import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response";
|
||||||
|
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||||
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
|
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||||
|
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||||
|
import { DialogService, ToastService } from "@bitwarden/components";
|
||||||
|
|
||||||
|
import { PaymentComponent } from "../shared/payment.component";
|
||||||
|
import { TaxInfoComponent } from "../shared/tax-info.component";
|
||||||
|
|
||||||
|
type ChangePlanDialogParams = {
|
||||||
|
organizationId: string;
|
||||||
|
subscription: OrganizationSubscriptionResponse;
|
||||||
|
productTierType: ProductTierType;
|
||||||
|
};
|
||||||
|
|
||||||
|
export enum ChangePlanDialogResultType {
|
||||||
|
Closed = "closed",
|
||||||
|
Submitted = "submitted",
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum PlanCardState {
|
||||||
|
Selected = "selected",
|
||||||
|
NotSelected = "not_selected",
|
||||||
|
Disabled = "disabled",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const openChangePlanDialog = (
|
||||||
|
dialogService: DialogService,
|
||||||
|
dialogConfig: DialogConfig<ChangePlanDialogParams>,
|
||||||
|
) =>
|
||||||
|
dialogService.open<ChangePlanDialogResultType, ChangePlanDialogParams>(
|
||||||
|
ChangePlanDialogComponent,
|
||||||
|
dialogConfig,
|
||||||
|
);
|
||||||
|
|
||||||
|
type PlanCard = {
|
||||||
|
name: string;
|
||||||
|
selected: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface OnSuccessArgs {
|
||||||
|
organizationId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
templateUrl: "./change-plan-dialog.component.html",
|
||||||
|
})
|
||||||
|
export class ChangePlanDialogComponent implements OnInit {
|
||||||
|
@ViewChild(PaymentComponent) paymentComponent: PaymentComponent;
|
||||||
|
@ViewChild(TaxInfoComponent) taxComponent: TaxInfoComponent;
|
||||||
|
|
||||||
|
@Input() acceptingSponsorship = false;
|
||||||
|
@Input() organizationId: string;
|
||||||
|
@Input() showFree = false;
|
||||||
|
@Input() showCancel = false;
|
||||||
|
selectedFile: File;
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
get productTier(): ProductTierType {
|
||||||
|
return this._productTier;
|
||||||
|
}
|
||||||
|
|
||||||
|
set productTier(product: ProductTierType) {
|
||||||
|
this._productTier = product;
|
||||||
|
this.formGroup?.controls?.productTier?.setValue(product);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _productTier = ProductTierType.Free;
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
get plan(): PlanType {
|
||||||
|
return this._plan;
|
||||||
|
}
|
||||||
|
|
||||||
|
set plan(plan: PlanType) {
|
||||||
|
this._plan = plan;
|
||||||
|
this.formGroup?.controls?.plan?.setValue(plan);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _plan = PlanType.Free;
|
||||||
|
@Input() providerId?: string;
|
||||||
|
@Output() onSuccess = new EventEmitter<OnSuccessArgs>();
|
||||||
|
@Output() onCanceled = new EventEmitter<void>();
|
||||||
|
@Output() onTrialBillingSuccess = new EventEmitter();
|
||||||
|
|
||||||
|
protected discountPercentage: number = 20;
|
||||||
|
protected discountPercentageFromSub: number;
|
||||||
|
protected loading = true;
|
||||||
|
protected planCards: PlanCard[];
|
||||||
|
protected ResultType = ChangePlanDialogResultType;
|
||||||
|
|
||||||
|
selfHosted = false;
|
||||||
|
productTypes = ProductTierType;
|
||||||
|
formPromise: Promise<string>;
|
||||||
|
singleOrgPolicyAppliesToActiveUser = false;
|
||||||
|
isInTrialFlow = false;
|
||||||
|
discount = 0;
|
||||||
|
|
||||||
|
formGroup = this.formBuilder.group({
|
||||||
|
name: [""],
|
||||||
|
billingEmail: ["", [Validators.email]],
|
||||||
|
businessOwned: [false],
|
||||||
|
premiumAccessAddon: [false],
|
||||||
|
additionalSeats: [0, [Validators.min(0), Validators.max(100000)]],
|
||||||
|
clientOwnerEmail: ["", [Validators.email]],
|
||||||
|
plan: [this.plan],
|
||||||
|
productTier: [this.productTier],
|
||||||
|
planInterval: [1],
|
||||||
|
});
|
||||||
|
|
||||||
|
planType: string;
|
||||||
|
selectedPlan: PlanResponse;
|
||||||
|
selectedInterval: number = 1;
|
||||||
|
planIntervals = PlanInterval;
|
||||||
|
passwordManagerPlans: PlanResponse[];
|
||||||
|
organization: Organization;
|
||||||
|
sub: OrganizationSubscriptionResponse;
|
||||||
|
billing: BillingResponse;
|
||||||
|
currentPlanName: string;
|
||||||
|
showPayment: boolean = false;
|
||||||
|
totalOpened: boolean = false;
|
||||||
|
currentPlan: PlanResponse;
|
||||||
|
|
||||||
|
private destroy$ = new Subject<void>();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(DIALOG_DATA) private dialogParams: ChangePlanDialogParams,
|
||||||
|
private dialogRef: DialogRef<ChangePlanDialogResultType>,
|
||||||
|
private toastService: ToastService,
|
||||||
|
private apiService: ApiService,
|
||||||
|
private i18nService: I18nService,
|
||||||
|
private cryptoService: CryptoService,
|
||||||
|
private router: Router,
|
||||||
|
private syncService: SyncService,
|
||||||
|
private policyService: PolicyService,
|
||||||
|
private organizationService: OrganizationService,
|
||||||
|
private messagingService: MessagingService,
|
||||||
|
private formBuilder: FormBuilder,
|
||||||
|
private organizationApiService: OrganizationApiServiceAbstraction,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async ngOnInit(): Promise<void> {
|
||||||
|
if (this.dialogParams.organizationId) {
|
||||||
|
this.currentPlanName = this.resolvePlanName(this.dialogParams.productTierType);
|
||||||
|
this.sub =
|
||||||
|
this.dialogParams.subscription ??
|
||||||
|
(await this.organizationApiService.getSubscription(this.dialogParams.organizationId));
|
||||||
|
this.organizationId = this.dialogParams.organizationId;
|
||||||
|
this.currentPlan = this.sub?.plan;
|
||||||
|
this.selectedPlan = this.sub?.plan;
|
||||||
|
this.organization = await this.organizationService.get(this.organizationId);
|
||||||
|
this.billing = await this.organizationApiService.getBilling(this.organizationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.selfHosted) {
|
||||||
|
const plans = await this.apiService.getPlans();
|
||||||
|
this.passwordManagerPlans = plans.data.filter((plan) => !!plan.PasswordManager);
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.productTier === ProductTierType.Enterprise ||
|
||||||
|
this.productTier === ProductTierType.Teams
|
||||||
|
) {
|
||||||
|
this.formGroup.controls.businessOwned.setValue(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.currentPlan && this.currentPlan.productTier !== ProductTierType.Enterprise) {
|
||||||
|
const upgradedPlan = this.passwordManagerPlans.find((plan) =>
|
||||||
|
this.currentPlan.productTier === ProductTierType.Free
|
||||||
|
? plan.type === PlanType.FamiliesAnnually
|
||||||
|
: plan.upgradeSortOrder == this.currentPlan.upgradeSortOrder + 1,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.plan = upgradedPlan.type;
|
||||||
|
this.productTier = upgradedPlan.productTier;
|
||||||
|
}
|
||||||
|
this.upgradeFlowPrefillForm();
|
||||||
|
|
||||||
|
this.policyService
|
||||||
|
.policyAppliesToActiveUser$(PolicyType.SingleOrg)
|
||||||
|
.pipe(takeUntil(this.destroy$))
|
||||||
|
.subscribe((policyAppliesToActiveUser) => {
|
||||||
|
this.singleOrgPolicyAppliesToActiveUser = policyAppliesToActiveUser;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!this.selfHosted) {
|
||||||
|
this.changedProduct();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.planCards = [
|
||||||
|
{
|
||||||
|
name: this.i18nService.t("planNameTeams"),
|
||||||
|
selected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: this.i18nService.t("planNameEnterprise"),
|
||||||
|
selected: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
this.formGroup
|
||||||
|
.get("planInterval")
|
||||||
|
.valueChanges.pipe(takeUntil(this.destroy$))
|
||||||
|
.subscribe((value: number) => (this.selectedInterval = value));
|
||||||
|
|
||||||
|
this.discountPercentageFromSub = this.sub?.customerDiscount?.percentOff;
|
||||||
|
|
||||||
|
this.setInitialPlanSelection();
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
setInitialPlanSelection() {
|
||||||
|
this.selectPlan(this.getPlanByType(ProductTierType.Enterprise));
|
||||||
|
}
|
||||||
|
|
||||||
|
getPlanByType(productTier: ProductTierType) {
|
||||||
|
return this.selectableProducts.find((product) => product.productTier === productTier);
|
||||||
|
}
|
||||||
|
|
||||||
|
planTypeChanged() {
|
||||||
|
this.selectPlan(this.getPlanByType(ProductTierType.Enterprise));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected getPlanIntervals() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: PlanInterval[PlanInterval.Annually],
|
||||||
|
value: PlanInterval.Annually,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: PlanInterval[PlanInterval.Monthly],
|
||||||
|
value: PlanInterval.Monthly,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected getPlanCardContainerClasses(plan: PlanResponse, index: number) {
|
||||||
|
let cardState: PlanCardState;
|
||||||
|
|
||||||
|
if (plan == this.selectedPlan) {
|
||||||
|
cardState = PlanCardState.Selected;
|
||||||
|
} else if (
|
||||||
|
this.selectedInterval === PlanInterval.Monthly &&
|
||||||
|
plan.productTier == ProductTierType.Families
|
||||||
|
) {
|
||||||
|
cardState = PlanCardState.Disabled;
|
||||||
|
} else {
|
||||||
|
cardState = PlanCardState.NotSelected;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (cardState) {
|
||||||
|
case PlanCardState.Selected: {
|
||||||
|
if (this.currentPlan.productTier === ProductTierType.Teams) {
|
||||||
|
return [
|
||||||
|
"tw-group",
|
||||||
|
"tw-cursor-pointer",
|
||||||
|
"tw-block",
|
||||||
|
"tw-rounded",
|
||||||
|
"tw-w-1/2",
|
||||||
|
"tw-border",
|
||||||
|
"tw-border-solid",
|
||||||
|
"tw-border-primary-600",
|
||||||
|
"hover:tw-border-primary-700",
|
||||||
|
"focus:tw-border-2",
|
||||||
|
"focus:tw-border-primary-700",
|
||||||
|
"focus:tw-rounded-lg",
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
return [
|
||||||
|
"tw-group",
|
||||||
|
"tw-cursor-pointer",
|
||||||
|
"tw-block",
|
||||||
|
"tw-rounded",
|
||||||
|
"tw-border",
|
||||||
|
"tw-border-solid",
|
||||||
|
"tw-border-primary-600",
|
||||||
|
"hover:tw-border-primary-700",
|
||||||
|
"focus:tw-border-2",
|
||||||
|
"focus:tw-border-primary-700",
|
||||||
|
"focus:tw-rounded-lg",
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case PlanCardState.NotSelected: {
|
||||||
|
return [
|
||||||
|
"tw-cursor-pointer",
|
||||||
|
"tw-block",
|
||||||
|
"tw-rounded",
|
||||||
|
"tw-border",
|
||||||
|
"tw-border-solid",
|
||||||
|
"tw-border-secondary-300",
|
||||||
|
"hover:tw-border-text-main",
|
||||||
|
"focus:tw-border-2",
|
||||||
|
"focus:tw-border-primary-700",
|
||||||
|
];
|
||||||
|
}
|
||||||
|
case PlanCardState.Disabled: {
|
||||||
|
return [
|
||||||
|
"tw-cursor-not-allowed",
|
||||||
|
"tw-bg-secondary-100",
|
||||||
|
"tw-font-normal",
|
||||||
|
"tw-bg-blur",
|
||||||
|
"tw-text-muted",
|
||||||
|
"tw-block",
|
||||||
|
"tw-rounded",
|
||||||
|
"tw-border",
|
||||||
|
"tw-border-solid",
|
||||||
|
"tw-border-1",
|
||||||
|
"tw-border-secondary-300",
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected selectPlan(plan: PlanResponse) {
|
||||||
|
if (
|
||||||
|
this.selectedInterval === PlanInterval.Monthly &&
|
||||||
|
plan.productTier == ProductTierType.Families
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.selectedPlan = plan;
|
||||||
|
this.formGroup.patchValue({ productTier: plan.productTier });
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
this.destroy$.next();
|
||||||
|
this.destroy$.complete();
|
||||||
|
}
|
||||||
|
|
||||||
|
get upgradeRequiresPaymentMethod() {
|
||||||
|
return (
|
||||||
|
this.organization?.productTierType === ProductTierType.Free &&
|
||||||
|
!this.showFree &&
|
||||||
|
!this.billing?.paymentSource
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
get selectedPlanInterval() {
|
||||||
|
return this.selectedPlan.isAnnual ? "year" : "month";
|
||||||
|
}
|
||||||
|
|
||||||
|
get selectableProducts() {
|
||||||
|
if (this.acceptingSponsorship) {
|
||||||
|
const familyPlan = this.passwordManagerPlans.find(
|
||||||
|
(plan) => plan.type === PlanType.FamiliesAnnually,
|
||||||
|
);
|
||||||
|
this.discount = familyPlan.PasswordManager.basePrice;
|
||||||
|
return [familyPlan];
|
||||||
|
}
|
||||||
|
|
||||||
|
const businessOwnedIsChecked = this.formGroup.controls.businessOwned.value;
|
||||||
|
|
||||||
|
const result = this.passwordManagerPlans.filter(
|
||||||
|
(plan) =>
|
||||||
|
plan.type !== PlanType.Custom &&
|
||||||
|
(!businessOwnedIsChecked || plan.canBeUsedByBusiness) &&
|
||||||
|
(this.showFree || plan.productTier !== ProductTierType.Free) &&
|
||||||
|
(plan.productTier === ProductTierType.Free ||
|
||||||
|
plan.productTier === ProductTierType.TeamsStarter ||
|
||||||
|
(this.selectedInterval === PlanInterval.Annually && plan.isAnnual) ||
|
||||||
|
(this.selectedInterval === PlanInterval.Monthly && !plan.isAnnual)) &&
|
||||||
|
(!this.currentPlan || this.currentPlan.upgradeSortOrder < plan.upgradeSortOrder) &&
|
||||||
|
this.planIsEnabled(plan),
|
||||||
|
);
|
||||||
|
|
||||||
|
result.sort((planA, planB) => planA.displaySortOrder - planB.displaySortOrder);
|
||||||
|
|
||||||
|
return result.reverse();
|
||||||
|
}
|
||||||
|
|
||||||
|
get selectablePlans() {
|
||||||
|
const selectedProductTierType = this.formGroup.controls.productTier.value;
|
||||||
|
const result =
|
||||||
|
this.passwordManagerPlans?.filter(
|
||||||
|
(plan) => plan.productTier === selectedProductTierType && this.planIsEnabled(plan),
|
||||||
|
) || [];
|
||||||
|
|
||||||
|
result.sort((planA, planB) => planA.displaySortOrder - planB.displaySortOrder);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
passwordManagerSeatTotal(plan: PlanResponse): number {
|
||||||
|
if (!plan.PasswordManager.hasAdditionalSeatsOption) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = plan.PasswordManager.seatPrice * Math.abs(this.organization.seats || 0);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
get passwordManagerSubtotal() {
|
||||||
|
let subTotal = this.selectedPlan.PasswordManager.basePrice;
|
||||||
|
if (this.selectedPlan.PasswordManager.hasAdditionalSeatsOption) {
|
||||||
|
subTotal += this.passwordManagerSeatTotal(this.selectedPlan);
|
||||||
|
}
|
||||||
|
if (this.selectedPlan.PasswordManager.hasPremiumAccessOption) {
|
||||||
|
subTotal += this.selectedPlan.PasswordManager.premiumAccessOptionPrice;
|
||||||
|
}
|
||||||
|
return subTotal - this.discount;
|
||||||
|
}
|
||||||
|
|
||||||
|
get taxCharges() {
|
||||||
|
return this.taxComponent != null && this.taxComponent.taxRate != null
|
||||||
|
? (this.taxComponent.taxRate / 100) * this.passwordManagerSubtotal
|
||||||
|
: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
get total() {
|
||||||
|
return this.passwordManagerSubtotal + this.taxCharges || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
get teamsStarterPlanIsAvailable() {
|
||||||
|
return this.selectablePlans.some((plan) => plan.type === PlanType.TeamsStarter);
|
||||||
|
}
|
||||||
|
|
||||||
|
changedProduct() {
|
||||||
|
const selectedPlan = this.selectablePlans[0];
|
||||||
|
|
||||||
|
this.setPlanType(selectedPlan.type);
|
||||||
|
this.handlePremiumAddonAccess(selectedPlan.PasswordManager.hasPremiumAccessOption);
|
||||||
|
this.handleAdditionalSeats(selectedPlan.PasswordManager.hasAdditionalSeatsOption);
|
||||||
|
}
|
||||||
|
|
||||||
|
setPlanType(planType: PlanType) {
|
||||||
|
this.formGroup.controls.plan.setValue(planType);
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePremiumAddonAccess(hasPremiumAccessOption: boolean) {
|
||||||
|
this.formGroup.controls.premiumAccessAddon.setValue(!hasPremiumAccessOption);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleAdditionalSeats(selectedPlanHasAdditionalSeatsOption: boolean) {
|
||||||
|
if (!selectedPlanHasAdditionalSeatsOption) {
|
||||||
|
this.formGroup.controls.additionalSeats.setValue(0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.currentPlan && !this.currentPlan.PasswordManager.hasAdditionalSeatsOption) {
|
||||||
|
this.formGroup.controls.additionalSeats.setValue(this.currentPlan.PasswordManager.baseSeats);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.organization) {
|
||||||
|
this.formGroup.controls.additionalSeats.setValue(this.organization.seats);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.formGroup.controls.additionalSeats.setValue(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
changedCountry() {
|
||||||
|
if (this.paymentComponent && this.taxComponent) {
|
||||||
|
this.paymentComponent!.hideBank = this.taxComponent?.taxFormGroup?.value.country !== "US";
|
||||||
|
// Bank Account payments are only available for US customers
|
||||||
|
if (
|
||||||
|
this.paymentComponent.hideBank &&
|
||||||
|
this.paymentComponent.method === PaymentMethodType.BankAccount
|
||||||
|
) {
|
||||||
|
this.paymentComponent.method = PaymentMethodType.Card;
|
||||||
|
this.paymentComponent.changeMethod();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
submit = async () => {
|
||||||
|
if (!this.taxComponent?.taxFormGroup.valid && this.taxComponent?.taxFormGroup.touched) {
|
||||||
|
this.taxComponent?.taxFormGroup.markAllAsTouched();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const doSubmit = async (): Promise<string> => {
|
||||||
|
let orgId: string = null;
|
||||||
|
orgId = await this.updateOrganization(orgId);
|
||||||
|
this.toastService.showToast({
|
||||||
|
variant: "success",
|
||||||
|
title: null,
|
||||||
|
message: this.i18nService.t("organizationUpgraded"),
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.apiService.refreshIdentityToken();
|
||||||
|
await this.syncService.fullSync(true);
|
||||||
|
|
||||||
|
if (!this.acceptingSponsorship && !this.isInTrialFlow) {
|
||||||
|
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
|
this.router.navigate(["/organizations/" + orgId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isInTrialFlow) {
|
||||||
|
this.onTrialBillingSuccess.emit({
|
||||||
|
orgId: orgId,
|
||||||
|
subLabelText: this.billingSubLabelText(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return orgId;
|
||||||
|
};
|
||||||
|
|
||||||
|
this.formPromise = doSubmit();
|
||||||
|
const organizationId = await this.formPromise;
|
||||||
|
this.onSuccess.emit({ organizationId: organizationId });
|
||||||
|
// TODO: No one actually listening to this message?
|
||||||
|
this.messagingService.send("organizationCreated", { organizationId });
|
||||||
|
this.dialogRef.close();
|
||||||
|
};
|
||||||
|
|
||||||
|
private async updateOrganization(orgId: string) {
|
||||||
|
const request = new OrganizationUpgradeRequest();
|
||||||
|
if (this.selectedPlan.productTier !== ProductTierType.Families) {
|
||||||
|
request.additionalSeats = this.organization.seats;
|
||||||
|
}
|
||||||
|
request.premiumAccessAddon =
|
||||||
|
this.selectedPlan.PasswordManager.hasPremiumAccessOption &&
|
||||||
|
this.formGroup.controls.premiumAccessAddon.value;
|
||||||
|
request.planType = this.selectedPlan.type;
|
||||||
|
if (this.showPayment) {
|
||||||
|
request.billingAddressCountry = this.taxComponent.taxFormGroup?.value.country;
|
||||||
|
request.billingAddressPostalCode = this.taxComponent.taxFormGroup?.value.postalCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.upgradeRequiresPaymentMethod || this.showPayment) {
|
||||||
|
const tokenResult = await this.paymentComponent.createPaymentToken();
|
||||||
|
const paymentRequest = new PaymentRequest();
|
||||||
|
paymentRequest.paymentToken = tokenResult[0];
|
||||||
|
paymentRequest.paymentMethodType = tokenResult[1];
|
||||||
|
paymentRequest.country = this.taxComponent.taxFormGroup?.value.country;
|
||||||
|
paymentRequest.postalCode = this.taxComponent.taxFormGroup?.value.postalCode;
|
||||||
|
await this.organizationApiService.updatePayment(this.organizationId, paymentRequest);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backfill pub/priv key if necessary
|
||||||
|
if (!this.organization.hasPublicAndPrivateKeys) {
|
||||||
|
const orgShareKey = await this.cryptoService.getOrgKey(this.organizationId);
|
||||||
|
const orgKeys = await this.cryptoService.makeKeyPair(orgShareKey);
|
||||||
|
request.keys = new OrganizationKeysRequest(orgKeys[0], orgKeys[1].encryptedString);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await this.organizationApiService.upgrade(this.organizationId, request);
|
||||||
|
if (!result.success && result.paymentIntentClientSecret != null) {
|
||||||
|
await this.paymentComponent.handleStripeCardPayment(result.paymentIntentClientSecret, null);
|
||||||
|
}
|
||||||
|
return this.organizationId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private billingSubLabelText(): string {
|
||||||
|
const selectedPlan = this.selectedPlan;
|
||||||
|
const price =
|
||||||
|
selectedPlan.PasswordManager.basePrice === 0
|
||||||
|
? selectedPlan.PasswordManager.seatPrice
|
||||||
|
: selectedPlan.PasswordManager.basePrice;
|
||||||
|
let text = "";
|
||||||
|
|
||||||
|
if (selectedPlan.isAnnual) {
|
||||||
|
text += `${this.i18nService.t("annual")} ($${price}/${this.i18nService.t("yr")})`;
|
||||||
|
} else {
|
||||||
|
text += `${this.i18nService.t("monthly")} ($${price}/${this.i18nService.t("monthAbbr")})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
private upgradeFlowPrefillForm() {
|
||||||
|
if (this.acceptingSponsorship) {
|
||||||
|
this.formGroup.controls.productTier.setValue(ProductTierType.Families);
|
||||||
|
this.changedProduct();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.currentPlan && this.currentPlan.productTier !== ProductTierType.Enterprise) {
|
||||||
|
const upgradedPlan = this.passwordManagerPlans.find((plan) => {
|
||||||
|
if (this.currentPlan.productTier === ProductTierType.Free) {
|
||||||
|
return plan.type === PlanType.FamiliesAnnually;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.currentPlan.productTier === ProductTierType.Families &&
|
||||||
|
!this.teamsStarterPlanIsAvailable
|
||||||
|
) {
|
||||||
|
return plan.type === PlanType.TeamsAnnually;
|
||||||
|
}
|
||||||
|
|
||||||
|
return plan.upgradeSortOrder === this.currentPlan.upgradeSortOrder + 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.plan = upgradedPlan.type;
|
||||||
|
this.productTier = upgradedPlan.productTier;
|
||||||
|
this.changedProduct();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private planIsEnabled(plan: PlanResponse) {
|
||||||
|
return !plan.disabled && !plan.legacyYear;
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleShowPayment() {
|
||||||
|
this.showPayment = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleTotalOpened() {
|
||||||
|
this.totalOpened = !this.totalOpened;
|
||||||
|
}
|
||||||
|
|
||||||
|
get paymentSourceClasses() {
|
||||||
|
if (this.billing.paymentSource == null) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
switch (this.billing.paymentSource.type) {
|
||||||
|
case PaymentMethodType.Card:
|
||||||
|
return ["bwi-credit-card"];
|
||||||
|
case PaymentMethodType.BankAccount:
|
||||||
|
return ["bwi-bank"];
|
||||||
|
case PaymentMethodType.Check:
|
||||||
|
return ["bwi-money"];
|
||||||
|
case PaymentMethodType.PayPal:
|
||||||
|
return ["bwi-paypal text-primary"];
|
||||||
|
default:
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resolvePlanName(productTier: ProductTierType) {
|
||||||
|
switch (productTier) {
|
||||||
|
case ProductTierType.Enterprise:
|
||||||
|
return this.i18nService.t("planNameEnterprise");
|
||||||
|
case ProductTierType.Free:
|
||||||
|
return this.i18nService.t("planNameFree");
|
||||||
|
case ProductTierType.Families:
|
||||||
|
return this.i18nService.t("planNameFamilies");
|
||||||
|
case ProductTierType.Teams:
|
||||||
|
return this.i18nService.t("planNameTeams");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,6 +7,7 @@ import { BillingSharedModule } from "../shared";
|
||||||
import { AdjustSubscription } from "./adjust-subscription.component";
|
import { AdjustSubscription } from "./adjust-subscription.component";
|
||||||
import { BillingSyncApiKeyComponent } from "./billing-sync-api-key.component";
|
import { BillingSyncApiKeyComponent } from "./billing-sync-api-key.component";
|
||||||
import { BillingSyncKeyComponent } from "./billing-sync-key.component";
|
import { BillingSyncKeyComponent } from "./billing-sync-key.component";
|
||||||
|
import { ChangePlanDialogComponent } from "./change-plan-dialog.component";
|
||||||
import { ChangePlanComponent } from "./change-plan.component";
|
import { ChangePlanComponent } from "./change-plan.component";
|
||||||
import { DownloadLicenceDialogComponent } from "./download-license.component";
|
import { DownloadLicenceDialogComponent } from "./download-license.component";
|
||||||
import { OrgBillingHistoryViewComponent } from "./organization-billing-history-view.component";
|
import { OrgBillingHistoryViewComponent } from "./organization-billing-history-view.component";
|
||||||
|
@ -40,6 +41,7 @@ import { SubscriptionStatusComponent } from "./subscription-status.component";
|
||||||
SecretsManagerSubscribeStandaloneComponent,
|
SecretsManagerSubscribeStandaloneComponent,
|
||||||
SubscriptionHiddenComponent,
|
SubscriptionHiddenComponent,
|
||||||
SubscriptionStatusComponent,
|
SubscriptionStatusComponent,
|
||||||
|
ChangePlanDialogComponent,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class OrganizationBillingModule {}
|
export class OrganizationBillingModule {}
|
||||||
|
|
|
@ -28,6 +28,7 @@ import {
|
||||||
} from "../shared/offboarding-survey.component";
|
} from "../shared/offboarding-survey.component";
|
||||||
|
|
||||||
import { BillingSyncApiKeyComponent } from "./billing-sync-api-key.component";
|
import { BillingSyncApiKeyComponent } from "./billing-sync-api-key.component";
|
||||||
|
import { ChangePlanDialogResultType, openChangePlanDialog } from "./change-plan-dialog.component";
|
||||||
import { DownloadLicenceDialogComponent } from "./download-license.component";
|
import { DownloadLicenceDialogComponent } from "./download-license.component";
|
||||||
import { ManageBilling } from "./icons/manage-billing.icon";
|
import { ManageBilling } from "./icons/manage-billing.icon";
|
||||||
import { SecretsManagerSubscriptionOptions } from "./sm-adjust-subscription.component";
|
import { SecretsManagerSubscriptionOptions } from "./sm-adjust-subscription.component";
|
||||||
|
@ -66,6 +67,10 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
|
||||||
FeatureFlag.EnableTimeThreshold,
|
FeatureFlag.EnableTimeThreshold,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
protected EnableUpgradePasswordManagerSub$ = this.configService.getFeatureFlag$(
|
||||||
|
FeatureFlag.EnableUpgradePasswordManagerSub,
|
||||||
|
);
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private apiService: ApiService,
|
private apiService: ApiService,
|
||||||
private platformUtilsService: PlatformUtilsService,
|
private platformUtilsService: PlatformUtilsService,
|
||||||
|
@ -383,7 +388,26 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
|
||||||
};
|
};
|
||||||
|
|
||||||
async changePlan() {
|
async changePlan() {
|
||||||
this.showChangePlan = !this.showChangePlan;
|
const EnableUpgradePasswordManagerSub = await firstValueFrom(
|
||||||
|
this.EnableUpgradePasswordManagerSub$,
|
||||||
|
);
|
||||||
|
if (EnableUpgradePasswordManagerSub) {
|
||||||
|
const reference = openChangePlanDialog(this.dialogService, {
|
||||||
|
data: {
|
||||||
|
organizationId: this.organizationId,
|
||||||
|
subscription: this.sub,
|
||||||
|
productTierType: this.userOrg.productTierType,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await lastValueFrom(reference.closed);
|
||||||
|
|
||||||
|
if (result === ChangePlanDialogResultType.Submitted) {
|
||||||
|
await this.load();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.showChangePlan = !this.showChangePlan;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
closeChangePlan() {
|
closeChangePlan() {
|
||||||
|
|
|
@ -394,7 +394,7 @@ export class TaxInfoComponent {
|
||||||
});
|
});
|
||||||
|
|
||||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
||||||
this.route.parent.parent.params.subscribe(async (params) => {
|
this.route.parent?.parent?.params.subscribe(async (params) => {
|
||||||
this.organizationId = params.organizationId;
|
this.organizationId = params.organizationId;
|
||||||
if (this.organizationId) {
|
if (this.organizationId) {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -8738,5 +8738,92 @@
|
||||||
},
|
},
|
||||||
"purchasedSeatsRemoved": {
|
"purchasedSeatsRemoved": {
|
||||||
"message": "purchased seats removed"
|
"message": "purchased seats removed"
|
||||||
|
},
|
||||||
|
"includesXMembers": {
|
||||||
|
"message": "for $COUNT$ member",
|
||||||
|
"placeholders": {
|
||||||
|
"count": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "5"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"costPerMember": {
|
||||||
|
"message": "$COST$",
|
||||||
|
"placeholders": {
|
||||||
|
"cost": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "$3"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"optionalOnPremHosting": {
|
||||||
|
"message": "Optional on-premises hosting"
|
||||||
|
},
|
||||||
|
"upgradeFreeOrganization": {
|
||||||
|
"message": "Upgrade your $NAME$ organization ",
|
||||||
|
"placeholders": {
|
||||||
|
"name": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Teams"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"includeSsoAuthenticationMessage": {
|
||||||
|
"message": "SSO Authentication"
|
||||||
|
},
|
||||||
|
"familiesPlanInvLimitReachedManageBilling": {
|
||||||
|
"message": "Families organizations may have up to $SEATCOUNT$ members. Upgrade to a paid plan to invite more members.",
|
||||||
|
"placeholders": {
|
||||||
|
"seatcount": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "6"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"familiesPlanInvLimitReachedNoManageBilling": {
|
||||||
|
"message": "Families organizations may have up to $SEATCOUNT$ members. Contact your organization owner to upgrade.",
|
||||||
|
"placeholders": {
|
||||||
|
"seatcount": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "6"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"upgradePlan": {
|
||||||
|
"message": "Upgrade your plan to invite more members and gain access to additional Bitwarden features"
|
||||||
|
},
|
||||||
|
"upgradeDiscount": {
|
||||||
|
"message": "Save $AMOUNT$%",
|
||||||
|
"placeholders": {
|
||||||
|
"amount": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"upgradeEnterpriseMessage": {
|
||||||
|
"message": "Advanced capabilities for larger businesses"
|
||||||
|
},
|
||||||
|
"upgradeTeamsMessage": {
|
||||||
|
"message": "Businesses looking for powerful security"
|
||||||
|
},
|
||||||
|
"teamsInviteMessage": {
|
||||||
|
"message": "Invite unlimited members"
|
||||||
|
},
|
||||||
|
"accessToCreateGroups": {
|
||||||
|
"message": "Access to create groups"
|
||||||
|
},
|
||||||
|
"syncGroupsAndUsersFromDirectory": {
|
||||||
|
"message": "Sync groups and users from a directory"
|
||||||
|
},
|
||||||
|
"upgradeFamilyMessage": {
|
||||||
|
"message": "Share with families and friends"
|
||||||
|
},
|
||||||
|
"accessToPremiumFeatures": {
|
||||||
|
"message": "Access to Premium features"
|
||||||
|
},
|
||||||
|
"additionalStorageGbMessage": {
|
||||||
|
"message": "GB additional storage"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,3 +5,4 @@ export * from "./transaction-type.enum";
|
||||||
export * from "./bitwarden-product-type.enum";
|
export * from "./bitwarden-product-type.enum";
|
||||||
export * from "./product-tier-type.enum";
|
export * from "./product-tier-type.enum";
|
||||||
export * from "./product-type.enum";
|
export * from "./product-type.enum";
|
||||||
|
export * from "./plan-interval.enum";
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
export enum PlanInterval {
|
||||||
|
Monthly = 0,
|
||||||
|
Annually = 1,
|
||||||
|
}
|
|
@ -5,3 +5,11 @@ export enum ProductTierType {
|
||||||
Enterprise = 3,
|
Enterprise = 3,
|
||||||
TeamsStarter = 4,
|
TeamsStarter = 4,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isNotSelfUpgradable(productType: ProductTierType): boolean {
|
||||||
|
return (
|
||||||
|
productType !== ProductTierType.Free &&
|
||||||
|
productType !== ProductTierType.TeamsStarter &&
|
||||||
|
productType !== ProductTierType.Families
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
@ -27,6 +27,7 @@ export enum FeatureFlag {
|
||||||
AC2828_ProviderPortalMembersPage = "AC-2828_provider-portal-members-page",
|
AC2828_ProviderPortalMembersPage = "AC-2828_provider-portal-members-page",
|
||||||
DeviceTrustLogging = "pm-8285-device-trust-logging",
|
DeviceTrustLogging = "pm-8285-device-trust-logging",
|
||||||
AuthenticatorTwoFactorToken = "authenticator-2fa-token",
|
AuthenticatorTwoFactorToken = "authenticator-2fa-token",
|
||||||
|
EnableUpgradePasswordManagerSub = "AC-2708-upgrade-password-manager-sub",
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AllowedFeatureFlagTypes = boolean | number | string;
|
export type AllowedFeatureFlagTypes = boolean | number | string;
|
||||||
|
@ -64,6 +65,7 @@ export const DefaultFeatureFlagValue = {
|
||||||
[FeatureFlag.AC2828_ProviderPortalMembersPage]: FALSE,
|
[FeatureFlag.AC2828_ProviderPortalMembersPage]: FALSE,
|
||||||
[FeatureFlag.DeviceTrustLogging]: FALSE,
|
[FeatureFlag.DeviceTrustLogging]: FALSE,
|
||||||
[FeatureFlag.AuthenticatorTwoFactorToken]: FALSE,
|
[FeatureFlag.AuthenticatorTwoFactorToken]: FALSE,
|
||||||
|
[FeatureFlag.EnableUpgradePasswordManagerSub]: FALSE,
|
||||||
} satisfies Record<FeatureFlag, AllowedFeatureFlagTypes>;
|
} satisfies Record<FeatureFlag, AllowedFeatureFlagTypes>;
|
||||||
|
|
||||||
export type DefaultFeatureFlagValueType = typeof DefaultFeatureFlagValue;
|
export type DefaultFeatureFlagValueType = typeof DefaultFeatureFlagValue;
|
||||||
|
|
Loading…
Reference in New Issue