mirror of
https://github.com/bitwarden/browser
synced 2025-12-29 14:43:31 +00:00
[PM 29079]Remove code for pm-24033-updat-premium-subscription-page (#17905)
* Remove the feature flag * delete and rename CloudHostedPremiumVNextComponent
This commit is contained in:
@@ -1,15 +1,11 @@
|
||||
import { inject, NgModule } from "@angular/core";
|
||||
import { RouterModule, Routes } from "@angular/router";
|
||||
import { map } from "rxjs";
|
||||
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { AccountPaymentDetailsComponent } from "@bitwarden/web-vault/app/billing/individual/payment-details/account-payment-details.component";
|
||||
import { SelfHostedPremiumComponent } from "@bitwarden/web-vault/app/billing/individual/premium/self-hosted-premium.component";
|
||||
|
||||
import { BillingHistoryViewComponent } from "./billing-history-view.component";
|
||||
import { CloudHostedPremiumVNextComponent } from "./premium/cloud-hosted-premium-vnext.component";
|
||||
import { CloudHostedPremiumComponent } from "./premium/cloud-hosted-premium.component";
|
||||
import { SubscriptionComponent } from "./subscription.component";
|
||||
import { UserSubscriptionComponent } from "./user-subscription.component";
|
||||
@@ -27,20 +23,15 @@ const routes: Routes = [
|
||||
data: { titleId: "premiumMembership" },
|
||||
},
|
||||
/**
|
||||
* Three-Route Matching Strategy for /premium:
|
||||
* Two-Route Matching Strategy for /premium:
|
||||
*
|
||||
* Routes are evaluated in order using canMatch guards. The first route that matches will be selected.
|
||||
*
|
||||
* 1. Self-Hosted Environment → SelfHostedPremiumComponent
|
||||
* - Matches when platformUtilsService.isSelfHost() === true
|
||||
*
|
||||
* 2. Cloud-Hosted + Feature Flag Enabled → CloudHostedPremiumVNextComponent
|
||||
* - Only evaluated if Route 1 doesn't match (not self-hosted)
|
||||
* - Matches when PM24033PremiumUpgradeNewDesign feature flag === true
|
||||
*
|
||||
* 3. Cloud-Hosted + Feature Flag Disabled → CloudHostedPremiumComponent (Fallback)
|
||||
* - No canMatch guard, so this always matches as the fallback route
|
||||
* - Used when neither Route 1 nor Route 2 match
|
||||
* 2. Cloud-Hosted (default) → CloudHostedPremiumComponent
|
||||
* - Evaluated when Route 1 doesn't match (not self-hosted)
|
||||
*/
|
||||
// Route 1: Self-Hosted -> SelfHostedPremiumComponent
|
||||
{
|
||||
@@ -54,22 +45,7 @@ const routes: Routes = [
|
||||
},
|
||||
],
|
||||
},
|
||||
// Route 2: Cloud Hosted + FF -> CloudHostedPremiumVNextComponent
|
||||
{
|
||||
path: "premium",
|
||||
component: CloudHostedPremiumVNextComponent,
|
||||
data: { titleId: "goPremium" },
|
||||
canMatch: [
|
||||
() => {
|
||||
const configService = inject(ConfigService);
|
||||
|
||||
return configService
|
||||
.getFeatureFlag$(FeatureFlag.PM24033PremiumUpgradeNewDesign)
|
||||
.pipe(map((flagValue) => flagValue === true));
|
||||
},
|
||||
],
|
||||
},
|
||||
// Route 3: Cloud Hosted + FF Disabled -> CloudHostedPremiumComponent (Fallback)
|
||||
// Route 2: Cloud Hosted (default) -> CloudHostedPremiumComponent
|
||||
{
|
||||
path: "premium",
|
||||
component: CloudHostedPremiumComponent,
|
||||
|
||||
@@ -12,7 +12,6 @@ import { BillingSharedModule } from "../shared";
|
||||
|
||||
import { BillingHistoryViewComponent } from "./billing-history-view.component";
|
||||
import { IndividualBillingRoutingModule } from "./individual-billing-routing.module";
|
||||
import { CloudHostedPremiumComponent } from "./premium/cloud-hosted-premium.component";
|
||||
import { SubscriptionComponent } from "./subscription.component";
|
||||
import { UserSubscriptionComponent } from "./user-subscription.component";
|
||||
|
||||
@@ -26,11 +25,6 @@ import { UserSubscriptionComponent } from "./user-subscription.component";
|
||||
PricingCardComponent,
|
||||
BaseCardComponent,
|
||||
],
|
||||
declarations: [
|
||||
SubscriptionComponent,
|
||||
BillingHistoryViewComponent,
|
||||
UserSubscriptionComponent,
|
||||
CloudHostedPremiumComponent,
|
||||
],
|
||||
declarations: [SubscriptionComponent, BillingHistoryViewComponent, UserSubscriptionComponent],
|
||||
})
|
||||
export class IndividualBillingModule {}
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
<div class="tw-max-w-3xl tw-mx-auto">
|
||||
<bit-section *ngIf="shouldShowNewDesign$ | async">
|
||||
<div class="tw-text-center">
|
||||
<div class="tw-mt-8 tw-mb-6">
|
||||
<span bitBadge variant="secondary" [truncate]="false">
|
||||
{{ "bitwardenFreeplanMessage" | i18n }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h2 class="tw-mt-2 tw-text-4xl">
|
||||
{{ "upgradeCompleteSecurity" | i18n }}
|
||||
</h2>
|
||||
<p class="tw-text-muted tw-mb-6 tw-mt-4">
|
||||
{{ "individualUpgradeDescriptionMessage" | i18n }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Two-Card Layout -->
|
||||
<div class="tw-grid tw-grid-cols-1 md:tw-grid-cols-2 tw-gap-6 tw-mt-6 tw-justify-center">
|
||||
<!-- Premium Card -->
|
||||
<div>
|
||||
@if (premiumCardData$ | async; as premiumData) {
|
||||
<billing-pricing-card
|
||||
[tagline]="'advancedOnlineSecurity' | i18n"
|
||||
[price]="{ amount: premiumData.price, cadence: 'monthly' }"
|
||||
[button]="{ type: 'primary', text: ('upgradeToPremium' | i18n) }"
|
||||
[features]="premiumData.features"
|
||||
(buttonClick)="openUpgradeDialog('Premium')"
|
||||
>
|
||||
<h3 slot="title" bitTypography="h3" class="tw-m-0">{{ "premium" | i18n }}</h3>
|
||||
</billing-pricing-card>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Families Card -->
|
||||
<div>
|
||||
@if (familiesCardData$ | async; as familiesData) {
|
||||
<billing-pricing-card
|
||||
[tagline]="'planDescFamiliesV2' | i18n"
|
||||
[price]="{ amount: familiesData.price, cadence: 'monthly' }"
|
||||
[button]="{ type: 'secondary', text: ('startFreeFamiliesTrial' | i18n) }"
|
||||
[features]="familiesData.features"
|
||||
(buttonClick)="openUpgradeDialog('Families')"
|
||||
>
|
||||
<h3 slot="title" bitTypography="h3" class="tw-m-0">{{ "families" | i18n }}</h3>
|
||||
</billing-pricing-card>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Business Plans Link -->
|
||||
<div class="tw-text-center tw-mt-6">
|
||||
<p class="tw-text-muted tw-mb-2 tw-italic">
|
||||
{{ "individualUpgradeTaxInformationMessage" | i18n }}
|
||||
</p>
|
||||
<a
|
||||
bitLink
|
||||
linkType="primary"
|
||||
href="https://bitwarden.com/pricing/business/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{{ "viewbusinessplans" | i18n }}
|
||||
<i class="bwi bwi-external-link tw-ml-1" aria-hidden="true"></i>
|
||||
</a>
|
||||
</div>
|
||||
</bit-section>
|
||||
</div>
|
||||
@@ -1,242 +0,0 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, DestroyRef, inject } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import {
|
||||
combineLatest,
|
||||
firstValueFrom,
|
||||
from,
|
||||
map,
|
||||
Observable,
|
||||
of,
|
||||
shareReplay,
|
||||
switchMap,
|
||||
take,
|
||||
} from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
|
||||
import { SubscriptionPricingServiceAbstraction } from "@bitwarden/common/billing/abstractions/subscription-pricing.service.abstraction";
|
||||
import {
|
||||
PersonalSubscriptionPricingTier,
|
||||
PersonalSubscriptionPricingTierIds,
|
||||
} from "@bitwarden/common/billing/types/subscription-pricing-tier";
|
||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||
import {
|
||||
BadgeModule,
|
||||
DialogService,
|
||||
LinkModule,
|
||||
SectionComponent,
|
||||
TypographyModule,
|
||||
} from "@bitwarden/components";
|
||||
import { PricingCardComponent } from "@bitwarden/pricing";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
|
||||
import { BitwardenSubscriber, mapAccountToSubscriber } from "../../types";
|
||||
import {
|
||||
UnifiedUpgradeDialogComponent,
|
||||
UnifiedUpgradeDialogParams,
|
||||
UnifiedUpgradeDialogResult,
|
||||
UnifiedUpgradeDialogStatus,
|
||||
UnifiedUpgradeDialogStep,
|
||||
} from "../upgrade/unified-upgrade-dialog/unified-upgrade-dialog.component";
|
||||
|
||||
const RouteParams = {
|
||||
callToAction: "callToAction",
|
||||
} as const;
|
||||
const RouteParamValues = {
|
||||
upgradeToPremium: "upgradeToPremium",
|
||||
} as const;
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
templateUrl: "./cloud-hosted-premium-vnext.component.html",
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
SectionComponent,
|
||||
BadgeModule,
|
||||
TypographyModule,
|
||||
LinkModule,
|
||||
I18nPipe,
|
||||
PricingCardComponent,
|
||||
],
|
||||
})
|
||||
export class CloudHostedPremiumVNextComponent {
|
||||
protected hasPremiumFromAnyOrganization$: Observable<boolean>;
|
||||
protected hasPremiumPersonally$: Observable<boolean>;
|
||||
protected shouldShowNewDesign$: Observable<boolean>;
|
||||
protected shouldShowUpgradeDialogOnInit$: Observable<boolean>;
|
||||
protected personalPricingTiers$: Observable<PersonalSubscriptionPricingTier[]>;
|
||||
protected premiumCardData$: Observable<{
|
||||
tier: PersonalSubscriptionPricingTier | undefined;
|
||||
price: number;
|
||||
features: string[];
|
||||
}>;
|
||||
protected familiesCardData$: Observable<{
|
||||
tier: PersonalSubscriptionPricingTier | undefined;
|
||||
price: number;
|
||||
features: string[];
|
||||
}>;
|
||||
protected subscriber!: BitwardenSubscriber;
|
||||
private destroyRef = inject(DestroyRef);
|
||||
|
||||
constructor(
|
||||
private accountService: AccountService,
|
||||
private apiService: ApiService,
|
||||
private dialogService: DialogService,
|
||||
private syncService: SyncService,
|
||||
private billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||
private subscriptionPricingService: SubscriptionPricingServiceAbstraction,
|
||||
private router: Router,
|
||||
private activatedRoute: ActivatedRoute,
|
||||
) {
|
||||
this.hasPremiumFromAnyOrganization$ = this.accountService.activeAccount$.pipe(
|
||||
switchMap((account) =>
|
||||
account
|
||||
? this.billingAccountProfileStateService.hasPremiumFromAnyOrganization$(account.id)
|
||||
: of(false),
|
||||
),
|
||||
);
|
||||
|
||||
this.hasPremiumPersonally$ = this.accountService.activeAccount$.pipe(
|
||||
switchMap((account) =>
|
||||
account
|
||||
? this.billingAccountProfileStateService.hasPremiumPersonally$(account.id)
|
||||
: of(false),
|
||||
),
|
||||
);
|
||||
|
||||
this.accountService.activeAccount$
|
||||
.pipe(mapAccountToSubscriber, takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe((subscriber) => {
|
||||
this.subscriber = subscriber;
|
||||
});
|
||||
|
||||
this.shouldShowNewDesign$ = combineLatest([
|
||||
this.hasPremiumFromAnyOrganization$,
|
||||
this.hasPremiumPersonally$,
|
||||
]).pipe(map(([hasOrgPremium, hasPersonalPremium]) => !hasOrgPremium && !hasPersonalPremium));
|
||||
|
||||
// redirect to user subscription page if they already have premium personally
|
||||
// redirect to individual vault if they already have premium from an org
|
||||
combineLatest([this.hasPremiumFromAnyOrganization$, this.hasPremiumPersonally$])
|
||||
.pipe(
|
||||
takeUntilDestroyed(this.destroyRef),
|
||||
switchMap(([hasPremiumFromOrg, hasPremiumPersonally]) => {
|
||||
if (hasPremiumPersonally) {
|
||||
return from(this.navigateToSubscriptionPage());
|
||||
}
|
||||
if (hasPremiumFromOrg) {
|
||||
return from(this.navigateToIndividualVault());
|
||||
}
|
||||
return of(true);
|
||||
}),
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
this.shouldShowUpgradeDialogOnInit$ = combineLatest([
|
||||
this.hasPremiumFromAnyOrganization$,
|
||||
this.hasPremiumPersonally$,
|
||||
this.activatedRoute.queryParams,
|
||||
]).pipe(
|
||||
map(([hasOrgPremium, hasPersonalPremium, queryParams]) => {
|
||||
const cta = queryParams[RouteParams.callToAction];
|
||||
return !hasOrgPremium && !hasPersonalPremium && cta === RouteParamValues.upgradeToPremium;
|
||||
}),
|
||||
);
|
||||
|
||||
this.personalPricingTiers$ =
|
||||
this.subscriptionPricingService.getPersonalSubscriptionPricingTiers$();
|
||||
|
||||
this.premiumCardData$ = this.personalPricingTiers$.pipe(
|
||||
map((tiers) => {
|
||||
const tier = tiers.find((t) => t.id === PersonalSubscriptionPricingTierIds.Premium);
|
||||
return {
|
||||
tier,
|
||||
price:
|
||||
tier?.passwordManager.type === "standalone" && tier.passwordManager.annualPrice
|
||||
? Number((tier.passwordManager.annualPrice / 12).toFixed(2))
|
||||
: 0,
|
||||
features: tier?.passwordManager.features.map((f) => f.value) || [],
|
||||
};
|
||||
}),
|
||||
shareReplay({ bufferSize: 1, refCount: true }),
|
||||
);
|
||||
|
||||
this.familiesCardData$ = this.personalPricingTiers$.pipe(
|
||||
map((tiers) => {
|
||||
const tier = tiers.find((t) => t.id === PersonalSubscriptionPricingTierIds.Families);
|
||||
return {
|
||||
tier,
|
||||
price:
|
||||
tier?.passwordManager.type === "packaged" && tier.passwordManager.annualPrice
|
||||
? Number((tier.passwordManager.annualPrice / 12).toFixed(2))
|
||||
: 0,
|
||||
features: tier?.passwordManager.features.map((f) => f.value) || [],
|
||||
};
|
||||
}),
|
||||
shareReplay({ bufferSize: 1, refCount: true }),
|
||||
);
|
||||
|
||||
this.shouldShowUpgradeDialogOnInit$
|
||||
.pipe(
|
||||
take(1),
|
||||
switchMap((shouldShowUpgradeDialogOnInit) => {
|
||||
if (shouldShowUpgradeDialogOnInit) {
|
||||
return from(this.openUpgradeDialog("Premium"));
|
||||
}
|
||||
// Return an Observable that completes immediately when dialog should not be shown
|
||||
return of(void 0);
|
||||
}),
|
||||
takeUntilDestroyed(this.destroyRef),
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
private navigateToSubscriptionPage = (): Promise<boolean> =>
|
||||
this.router.navigate(["../user-subscription"], { relativeTo: this.activatedRoute });
|
||||
|
||||
private navigateToIndividualVault = (): Promise<boolean> => this.router.navigate(["/vault"]);
|
||||
|
||||
finalizeUpgrade = async () => {
|
||||
await this.apiService.refreshIdentityToken();
|
||||
await this.syncService.fullSync(true);
|
||||
};
|
||||
|
||||
protected async openUpgradeDialog(planType: "Premium" | "Families"): Promise<void> {
|
||||
const account = await firstValueFrom(this.accountService.activeAccount$);
|
||||
if (!account) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedPlan =
|
||||
planType === "Premium"
|
||||
? PersonalSubscriptionPricingTierIds.Premium
|
||||
: PersonalSubscriptionPricingTierIds.Families;
|
||||
|
||||
const dialogParams: UnifiedUpgradeDialogParams = {
|
||||
account,
|
||||
initialStep: UnifiedUpgradeDialogStep.Payment,
|
||||
selectedPlan: selectedPlan,
|
||||
redirectOnCompletion: true,
|
||||
};
|
||||
|
||||
const dialogRef = UnifiedUpgradeDialogComponent.open(this.dialogService, {
|
||||
data: dialogParams,
|
||||
});
|
||||
|
||||
dialogRef.closed
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe((result: UnifiedUpgradeDialogResult | undefined) => {
|
||||
if (
|
||||
result?.status === UnifiedUpgradeDialogStatus.UpgradedToPremium ||
|
||||
result?.status === UnifiedUpgradeDialogStatus.UpgradedToFamilies
|
||||
) {
|
||||
void this.finalizeUpgrade();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,141 +1,68 @@
|
||||
@if (isLoadingPrices$ | async) {
|
||||
<ng-container>
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin tw-text-muted"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
|
||||
</ng-container>
|
||||
} @else {
|
||||
<bit-container>
|
||||
<bit-section>
|
||||
<h2 bitTypography="h2">{{ "goPremium" | i18n }}</h2>
|
||||
<bit-callout
|
||||
type="info"
|
||||
*ngIf="hasPremiumFromAnyOrganization$ | async"
|
||||
title="{{ 'youHavePremiumAccess' | i18n }}"
|
||||
icon="bwi bwi-star-f"
|
||||
<div class="tw-max-w-3xl tw-mx-auto">
|
||||
<bit-section *ngIf="shouldShowNewDesign$ | async">
|
||||
<div class="tw-text-center">
|
||||
<div class="tw-mt-8 tw-mb-6">
|
||||
<span bitBadge variant="secondary" [truncate]="false">
|
||||
{{ "bitwardenFreeplanMessage" | i18n }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h2 class="tw-mt-2 tw-text-4xl">
|
||||
{{ "upgradeCompleteSecurity" | i18n }}
|
||||
</h2>
|
||||
<p class="tw-text-muted tw-mb-6 tw-mt-4">
|
||||
{{ "individualUpgradeDescriptionMessage" | i18n }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Two-Card Layout -->
|
||||
<div class="tw-grid tw-grid-cols-1 md:tw-grid-cols-2 tw-gap-6 tw-mt-6 tw-justify-center">
|
||||
<!-- Premium Card -->
|
||||
<div>
|
||||
@if (premiumCardData$ | async; as premiumData) {
|
||||
<billing-pricing-card
|
||||
[tagline]="'advancedOnlineSecurity' | i18n"
|
||||
[price]="{ amount: premiumData.price, cadence: 'monthly' }"
|
||||
[button]="{ type: 'primary', text: ('upgradeToPremium' | i18n) }"
|
||||
[features]="premiumData.features"
|
||||
(buttonClick)="openUpgradeDialog('Premium')"
|
||||
>
|
||||
<h3 slot="title" bitTypography="h3" class="tw-m-0">{{ "premium" | i18n }}</h3>
|
||||
</billing-pricing-card>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Families Card -->
|
||||
<div>
|
||||
@if (familiesCardData$ | async; as familiesData) {
|
||||
<billing-pricing-card
|
||||
[tagline]="'planDescFamiliesV2' | i18n"
|
||||
[price]="{ amount: familiesData.price, cadence: 'monthly' }"
|
||||
[button]="{ type: 'secondary', text: ('startFreeFamiliesTrial' | i18n) }"
|
||||
[features]="familiesData.features"
|
||||
(buttonClick)="openUpgradeDialog('Families')"
|
||||
>
|
||||
<h3 slot="title" bitTypography="h3" class="tw-m-0">{{ "families" | i18n }}</h3>
|
||||
</billing-pricing-card>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Business Plans Link -->
|
||||
<div class="tw-text-center tw-mt-6">
|
||||
<p class="tw-text-muted tw-mb-2 tw-italic">
|
||||
{{ "individualUpgradeTaxInformationMessage" | i18n }}
|
||||
</p>
|
||||
<a
|
||||
bitLink
|
||||
linkType="primary"
|
||||
href="https://bitwarden.com/pricing/business/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{{ "alreadyPremiumFromOrg" | i18n }}
|
||||
</bit-callout>
|
||||
<bit-callout type="success">
|
||||
<p>{{ "premiumUpgradeUnlockFeatures" | i18n }}</p>
|
||||
<ul class="bwi-ul">
|
||||
<li>
|
||||
<i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i>
|
||||
{{ "premiumSignUpStorageV2" | i18n: `${(providedStorageGb$ | async)} GB` }}
|
||||
</li>
|
||||
<li>
|
||||
<i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i>
|
||||
{{ "premiumSignUpTwoStepOptions" | i18n }}
|
||||
</li>
|
||||
<li>
|
||||
<i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i>
|
||||
{{ "premiumSignUpEmergency" | i18n }}
|
||||
</li>
|
||||
<li>
|
||||
<i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i>
|
||||
{{ "premiumSignUpReports" | i18n }}
|
||||
</li>
|
||||
<li>
|
||||
<i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i>
|
||||
{{ "premiumSignUpTotp" | i18n }}
|
||||
</li>
|
||||
<li>
|
||||
<i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i>
|
||||
{{ "premiumSignUpSupport" | i18n }}
|
||||
</li>
|
||||
<li>
|
||||
<i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i>
|
||||
{{ "premiumSignUpFuture" | i18n }}
|
||||
</li>
|
||||
</ul>
|
||||
<p bitTypography="body1" class="tw-mb-0">
|
||||
{{
|
||||
"premiumPriceWithFamilyPlan"
|
||||
| i18n: (premiumPrice$ | async | currency: "$") : familyPlanMaxUserCount
|
||||
}}
|
||||
<a
|
||||
bitLink
|
||||
linkType="primary"
|
||||
routerLink="/create-organization"
|
||||
[queryParams]="{ plan: 'families' }"
|
||||
>
|
||||
{{ "bitwardenFamiliesPlan" | i18n }}
|
||||
</a>
|
||||
</p>
|
||||
</bit-callout>
|
||||
</bit-section>
|
||||
<form [formGroup]="formGroup" [bitSubmit]="submitPayment">
|
||||
<bit-section>
|
||||
<h2 bitTypography="h2">{{ "addons" | i18n }}</h2>
|
||||
<div class="tw-grid tw-grid-cols-12 tw-gap-4">
|
||||
<bit-form-field class="tw-col-span-6">
|
||||
<bit-label>{{ "additionalStorageGb" | i18n }}</bit-label>
|
||||
<input
|
||||
bitInput
|
||||
formControlName="additionalStorage"
|
||||
type="number"
|
||||
step="1"
|
||||
placeholder="{{ 'additionalStorageGbDesc' | i18n }}"
|
||||
/>
|
||||
<bit-hint>{{
|
||||
"additionalStorageIntervalDesc"
|
||||
| i18n
|
||||
: `${(providedStorageGb$ | async)} GB`
|
||||
: (storagePrice$ | async | currency: "$")
|
||||
: ("year" | i18n)
|
||||
}}</bit-hint>
|
||||
</bit-form-field>
|
||||
</div>
|
||||
</bit-section>
|
||||
<bit-section>
|
||||
<h2 bitTypography="h2">{{ "summary" | i18n }}</h2>
|
||||
{{ "premiumMembership" | i18n }}: {{ premiumPrice$ | async | currency: "$" }} <br />
|
||||
{{ "additionalStorageGb" | i18n }}: {{ formGroup.value.additionalStorage || 0 }} GB ×
|
||||
{{ storagePrice$ | async | currency: "$" }} =
|
||||
{{ storageCost$ | async | currency: "$" }}
|
||||
<hr class="tw-my-3" />
|
||||
</bit-section>
|
||||
<bit-section>
|
||||
<h3 bitTypography="h2">{{ "paymentInformation" | i18n }}</h3>
|
||||
<div class="tw-mb-4">
|
||||
<app-enter-payment-method
|
||||
[group]="formGroup.controls.paymentMethod"
|
||||
[showBankAccount]="false"
|
||||
[showAccountCredit]="true"
|
||||
[hasEnoughAccountCredit]="hasEnoughAccountCredit$ | async"
|
||||
>
|
||||
</app-enter-payment-method>
|
||||
<app-enter-billing-address
|
||||
[group]="formGroup.controls.billingAddress"
|
||||
[scenario]="{ type: 'checkout', supportsTaxId: false }"
|
||||
>
|
||||
</app-enter-billing-address>
|
||||
</div>
|
||||
<div class="tw-mb-4">
|
||||
<div class="tw-text-muted tw-text-sm tw-flex tw-flex-col">
|
||||
<span>{{ "planPrice" | i18n }}: {{ subtotal$ | async | currency: "USD $" }}</span>
|
||||
<span>{{ "estimatedTax" | i18n }}: {{ tax$ | async | currency: "USD $" }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<hr class="tw-my-1 tw-w-1/4 tw-ml-0" />
|
||||
<p bitTypography="body1">
|
||||
<strong>{{ "total" | i18n }}:</strong> {{ total$ | async | currency: "USD $" }}/{{
|
||||
"year" | i18n
|
||||
}}
|
||||
</p>
|
||||
<button
|
||||
type="submit"
|
||||
buttonType="primary"
|
||||
bitButton
|
||||
bitFormButton
|
||||
[disabled]="!(hasEnoughAccountCredit$ | async)"
|
||||
>
|
||||
{{ "submit" | i18n }}
|
||||
</button>
|
||||
</bit-section>
|
||||
</form>
|
||||
</bit-container>
|
||||
}
|
||||
{{ "viewbusinessplans" | i18n }}
|
||||
<i class="bwi bwi-external-link tw-ml-1" aria-hidden="true"></i>
|
||||
</a>
|
||||
</div>
|
||||
</bit-section>
|
||||
</div>
|
||||
|
||||
@@ -1,243 +1,242 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component, ViewChild } from "@angular/core";
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, DestroyRef, inject } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { FormControl, FormGroup, Validators } from "@angular/forms";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import {
|
||||
catchError,
|
||||
combineLatest,
|
||||
concatMap,
|
||||
filter,
|
||||
firstValueFrom,
|
||||
from,
|
||||
map,
|
||||
Observable,
|
||||
of,
|
||||
shareReplay,
|
||||
startWith,
|
||||
switchMap,
|
||||
take,
|
||||
} from "rxjs";
|
||||
import { debounceTime } from "rxjs/operators";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
|
||||
import { SubscriptionPricingServiceAbstraction } from "@bitwarden/common/billing/abstractions/subscription-pricing.service.abstraction";
|
||||
import { PaymentMethodType } from "@bitwarden/common/billing/enums";
|
||||
import { PersonalSubscriptionPricingTierIds } from "@bitwarden/common/billing/types/subscription-pricing-tier";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import {
|
||||
PersonalSubscriptionPricingTier,
|
||||
PersonalSubscriptionPricingTierIds,
|
||||
} from "@bitwarden/common/billing/types/subscription-pricing-tier";
|
||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
import { SubscriberBillingClient, TaxClient } from "@bitwarden/web-vault/app/billing/clients";
|
||||
import {
|
||||
EnterBillingAddressComponent,
|
||||
EnterPaymentMethodComponent,
|
||||
getBillingAddressFromForm,
|
||||
} from "@bitwarden/web-vault/app/billing/payment/components";
|
||||
BadgeModule,
|
||||
DialogService,
|
||||
LinkModule,
|
||||
SectionComponent,
|
||||
TypographyModule,
|
||||
} from "@bitwarden/components";
|
||||
import { PricingCardComponent } from "@bitwarden/pricing";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
|
||||
import { BitwardenSubscriber, mapAccountToSubscriber } from "../../types";
|
||||
import {
|
||||
NonTokenizablePaymentMethods,
|
||||
tokenizablePaymentMethodToLegacyEnum,
|
||||
} from "@bitwarden/web-vault/app/billing/payment/types";
|
||||
import { mapAccountToSubscriber } from "@bitwarden/web-vault/app/billing/types";
|
||||
UnifiedUpgradeDialogComponent,
|
||||
UnifiedUpgradeDialogParams,
|
||||
UnifiedUpgradeDialogResult,
|
||||
UnifiedUpgradeDialogStatus,
|
||||
UnifiedUpgradeDialogStep,
|
||||
} from "../upgrade/unified-upgrade-dialog/unified-upgrade-dialog.component";
|
||||
|
||||
const RouteParams = {
|
||||
callToAction: "callToAction",
|
||||
} as const;
|
||||
const RouteParamValues = {
|
||||
upgradeToPremium: "upgradeToPremium",
|
||||
} as const;
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
templateUrl: "./cloud-hosted-premium.component.html",
|
||||
standalone: false,
|
||||
providers: [SubscriberBillingClient, TaxClient],
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
SectionComponent,
|
||||
BadgeModule,
|
||||
TypographyModule,
|
||||
LinkModule,
|
||||
I18nPipe,
|
||||
PricingCardComponent,
|
||||
],
|
||||
})
|
||||
export class CloudHostedPremiumComponent {
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@ViewChild(EnterPaymentMethodComponent) enterPaymentMethodComponent!: EnterPaymentMethodComponent;
|
||||
|
||||
protected hasPremiumFromAnyOrganization$: Observable<boolean>;
|
||||
protected hasEnoughAccountCredit$: Observable<boolean>;
|
||||
|
||||
protected formGroup = new FormGroup({
|
||||
additionalStorage: new FormControl<number>(0, [Validators.min(0), Validators.max(99)]),
|
||||
paymentMethod: EnterPaymentMethodComponent.getFormGroup(),
|
||||
billingAddress: EnterBillingAddressComponent.getFormGroup(),
|
||||
});
|
||||
|
||||
premiumPrices$ = this.subscriptionPricingService.getPersonalSubscriptionPricingTiers$().pipe(
|
||||
map((tiers) => {
|
||||
const premiumPlan = tiers.find(
|
||||
(tier) => tier.id === PersonalSubscriptionPricingTierIds.Premium,
|
||||
);
|
||||
|
||||
if (!premiumPlan) {
|
||||
throw new Error("Could not find Premium plan");
|
||||
}
|
||||
|
||||
return {
|
||||
seat: premiumPlan.passwordManager.annualPrice,
|
||||
storage: premiumPlan.passwordManager.annualPricePerAdditionalStorageGB,
|
||||
providedStorageGb: premiumPlan.passwordManager.providedStorageGB,
|
||||
};
|
||||
}),
|
||||
shareReplay({ bufferSize: 1, refCount: true }),
|
||||
);
|
||||
|
||||
premiumPrice$ = this.premiumPrices$.pipe(map((prices) => prices.seat));
|
||||
|
||||
storagePrice$ = this.premiumPrices$.pipe(map((prices) => prices.storage));
|
||||
|
||||
providedStorageGb$ = this.premiumPrices$.pipe(map((prices) => prices.providedStorageGb));
|
||||
|
||||
protected isLoadingPrices$ = this.premiumPrices$.pipe(
|
||||
map(() => false),
|
||||
startWith(true),
|
||||
catchError(() => of(false)),
|
||||
);
|
||||
|
||||
storageCost$ = combineLatest([
|
||||
this.storagePrice$,
|
||||
this.formGroup.controls.additionalStorage.valueChanges.pipe(
|
||||
startWith(this.formGroup.value.additionalStorage),
|
||||
),
|
||||
]).pipe(map(([storagePrice, additionalStorage]) => storagePrice * additionalStorage));
|
||||
|
||||
subtotal$ = combineLatest([this.premiumPrice$, this.storageCost$]).pipe(
|
||||
map(([premiumPrice, storageCost]) => premiumPrice + storageCost),
|
||||
);
|
||||
|
||||
tax$ = this.formGroup.valueChanges.pipe(
|
||||
filter(() => this.formGroup.valid),
|
||||
debounceTime(1000),
|
||||
switchMap(async () => {
|
||||
const billingAddress = getBillingAddressFromForm(this.formGroup.controls.billingAddress);
|
||||
const taxAmounts = await this.taxClient.previewTaxForPremiumSubscriptionPurchase(
|
||||
this.formGroup.value.additionalStorage,
|
||||
billingAddress,
|
||||
);
|
||||
return taxAmounts.tax;
|
||||
}),
|
||||
startWith(0),
|
||||
);
|
||||
|
||||
total$ = combineLatest([this.subtotal$, this.tax$]).pipe(
|
||||
map(([subtotal, tax]) => subtotal + tax),
|
||||
);
|
||||
|
||||
protected cloudWebVaultURL: string;
|
||||
protected readonly familyPlanMaxUserCount = 6;
|
||||
protected hasPremiumPersonally$: Observable<boolean>;
|
||||
protected shouldShowNewDesign$: Observable<boolean>;
|
||||
protected shouldShowUpgradeDialogOnInit$: Observable<boolean>;
|
||||
protected personalPricingTiers$: Observable<PersonalSubscriptionPricingTier[]>;
|
||||
protected premiumCardData$: Observable<{
|
||||
tier: PersonalSubscriptionPricingTier | undefined;
|
||||
price: number;
|
||||
features: string[];
|
||||
}>;
|
||||
protected familiesCardData$: Observable<{
|
||||
tier: PersonalSubscriptionPricingTier | undefined;
|
||||
price: number;
|
||||
features: string[];
|
||||
}>;
|
||||
protected subscriber!: BitwardenSubscriber;
|
||||
private destroyRef = inject(DestroyRef);
|
||||
|
||||
constructor(
|
||||
private activatedRoute: ActivatedRoute,
|
||||
private apiService: ApiService,
|
||||
private billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||
private environmentService: EnvironmentService,
|
||||
private i18nService: I18nService,
|
||||
private router: Router,
|
||||
private syncService: SyncService,
|
||||
private toastService: ToastService,
|
||||
private accountService: AccountService,
|
||||
private subscriberBillingClient: SubscriberBillingClient,
|
||||
private taxClient: TaxClient,
|
||||
private apiService: ApiService,
|
||||
private dialogService: DialogService,
|
||||
private syncService: SyncService,
|
||||
private billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||
private subscriptionPricingService: SubscriptionPricingServiceAbstraction,
|
||||
private router: Router,
|
||||
private activatedRoute: ActivatedRoute,
|
||||
) {
|
||||
this.hasPremiumFromAnyOrganization$ = this.accountService.activeAccount$.pipe(
|
||||
switchMap((account) =>
|
||||
this.billingAccountProfileStateService.hasPremiumFromAnyOrganization$(account.id),
|
||||
account
|
||||
? this.billingAccountProfileStateService.hasPremiumFromAnyOrganization$(account.id)
|
||||
: of(false),
|
||||
),
|
||||
);
|
||||
|
||||
const accountCredit$ = this.accountService.activeAccount$.pipe(
|
||||
mapAccountToSubscriber,
|
||||
switchMap((account) => this.subscriberBillingClient.getCredit(account)),
|
||||
this.hasPremiumPersonally$ = this.accountService.activeAccount$.pipe(
|
||||
switchMap((account) =>
|
||||
account
|
||||
? this.billingAccountProfileStateService.hasPremiumPersonally$(account.id)
|
||||
: of(false),
|
||||
),
|
||||
);
|
||||
|
||||
this.hasEnoughAccountCredit$ = combineLatest([
|
||||
accountCredit$,
|
||||
this.total$,
|
||||
this.formGroup.controls.paymentMethod.controls.type.valueChanges.pipe(
|
||||
startWith(this.formGroup.value.paymentMethod.type),
|
||||
),
|
||||
]).pipe(
|
||||
map(([credit, total, paymentMethod]) => {
|
||||
if (paymentMethod !== NonTokenizablePaymentMethods.accountCredit) {
|
||||
return true;
|
||||
}
|
||||
return credit >= total;
|
||||
}),
|
||||
);
|
||||
this.accountService.activeAccount$
|
||||
.pipe(mapAccountToSubscriber, takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe((subscriber) => {
|
||||
this.subscriber = subscriber;
|
||||
});
|
||||
|
||||
combineLatest([
|
||||
this.accountService.activeAccount$.pipe(
|
||||
switchMap((account) =>
|
||||
this.billingAccountProfileStateService.hasPremiumPersonally$(account.id),
|
||||
),
|
||||
),
|
||||
this.environmentService.cloudWebVaultUrl$,
|
||||
])
|
||||
this.shouldShowNewDesign$ = combineLatest([
|
||||
this.hasPremiumFromAnyOrganization$,
|
||||
this.hasPremiumPersonally$,
|
||||
]).pipe(map(([hasOrgPremium, hasPersonalPremium]) => !hasOrgPremium && !hasPersonalPremium));
|
||||
|
||||
// redirect to user subscription page if they already have premium personally
|
||||
// redirect to individual vault if they already have premium from an org
|
||||
combineLatest([this.hasPremiumFromAnyOrganization$, this.hasPremiumPersonally$])
|
||||
.pipe(
|
||||
takeUntilDestroyed(),
|
||||
concatMap(([hasPremiumPersonally, cloudWebVaultURL]) => {
|
||||
takeUntilDestroyed(this.destroyRef),
|
||||
switchMap(([hasPremiumFromOrg, hasPremiumPersonally]) => {
|
||||
if (hasPremiumPersonally) {
|
||||
return from(this.navigateToSubscriptionPage());
|
||||
}
|
||||
|
||||
this.cloudWebVaultURL = cloudWebVaultURL;
|
||||
if (hasPremiumFromOrg) {
|
||||
return from(this.navigateToIndividualVault());
|
||||
}
|
||||
return of(true);
|
||||
}),
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
this.shouldShowUpgradeDialogOnInit$ = combineLatest([
|
||||
this.hasPremiumFromAnyOrganization$,
|
||||
this.hasPremiumPersonally$,
|
||||
this.activatedRoute.queryParams,
|
||||
]).pipe(
|
||||
map(([hasOrgPremium, hasPersonalPremium, queryParams]) => {
|
||||
const cta = queryParams[RouteParams.callToAction];
|
||||
return !hasOrgPremium && !hasPersonalPremium && cta === RouteParamValues.upgradeToPremium;
|
||||
}),
|
||||
);
|
||||
|
||||
this.personalPricingTiers$ =
|
||||
this.subscriptionPricingService.getPersonalSubscriptionPricingTiers$();
|
||||
|
||||
this.premiumCardData$ = this.personalPricingTiers$.pipe(
|
||||
map((tiers) => {
|
||||
const tier = tiers.find((t) => t.id === PersonalSubscriptionPricingTierIds.Premium);
|
||||
return {
|
||||
tier,
|
||||
price:
|
||||
tier?.passwordManager.type === "standalone" && tier.passwordManager.annualPrice
|
||||
? Number((tier.passwordManager.annualPrice / 12).toFixed(2))
|
||||
: 0,
|
||||
features: tier?.passwordManager.features.map((f) => f.value) || [],
|
||||
};
|
||||
}),
|
||||
shareReplay({ bufferSize: 1, refCount: true }),
|
||||
);
|
||||
|
||||
this.familiesCardData$ = this.personalPricingTiers$.pipe(
|
||||
map((tiers) => {
|
||||
const tier = tiers.find((t) => t.id === PersonalSubscriptionPricingTierIds.Families);
|
||||
return {
|
||||
tier,
|
||||
price:
|
||||
tier?.passwordManager.type === "packaged" && tier.passwordManager.annualPrice
|
||||
? Number((tier.passwordManager.annualPrice / 12).toFixed(2))
|
||||
: 0,
|
||||
features: tier?.passwordManager.features.map((f) => f.value) || [],
|
||||
};
|
||||
}),
|
||||
shareReplay({ bufferSize: 1, refCount: true }),
|
||||
);
|
||||
|
||||
this.shouldShowUpgradeDialogOnInit$
|
||||
.pipe(
|
||||
take(1),
|
||||
switchMap((shouldShowUpgradeDialogOnInit) => {
|
||||
if (shouldShowUpgradeDialogOnInit) {
|
||||
return from(this.openUpgradeDialog("Premium"));
|
||||
}
|
||||
// Return an Observable that completes immediately when dialog should not be shown
|
||||
return of(void 0);
|
||||
}),
|
||||
takeUntilDestroyed(this.destroyRef),
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
private navigateToSubscriptionPage = (): Promise<boolean> =>
|
||||
this.router.navigate(["../user-subscription"], { relativeTo: this.activatedRoute });
|
||||
|
||||
private navigateToIndividualVault = (): Promise<boolean> => this.router.navigate(["/vault"]);
|
||||
|
||||
finalizeUpgrade = async () => {
|
||||
await this.apiService.refreshIdentityToken();
|
||||
await this.syncService.fullSync(true);
|
||||
};
|
||||
|
||||
postFinalizeUpgrade = async () => {
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: this.i18nService.t("premiumUpdated"),
|
||||
});
|
||||
await this.navigateToSubscriptionPage();
|
||||
};
|
||||
|
||||
navigateToSubscriptionPage = (): Promise<boolean> =>
|
||||
this.router.navigate(["../user-subscription"], { relativeTo: this.activatedRoute });
|
||||
|
||||
submitPayment = async (): Promise<void> => {
|
||||
if (this.formGroup.invalid) {
|
||||
protected async openUpgradeDialog(planType: "Premium" | "Families"): Promise<void> {
|
||||
const account = await firstValueFrom(this.accountService.activeAccount$);
|
||||
if (!account) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if account credit is selected
|
||||
const selectedPaymentType = this.formGroup.value.paymentMethod.type;
|
||||
const selectedPlan =
|
||||
planType === "Premium"
|
||||
? PersonalSubscriptionPricingTierIds.Premium
|
||||
: PersonalSubscriptionPricingTierIds.Families;
|
||||
|
||||
let paymentMethodType: number;
|
||||
let paymentToken: string;
|
||||
const dialogParams: UnifiedUpgradeDialogParams = {
|
||||
account,
|
||||
initialStep: UnifiedUpgradeDialogStep.Payment,
|
||||
selectedPlan: selectedPlan,
|
||||
redirectOnCompletion: true,
|
||||
};
|
||||
|
||||
if (selectedPaymentType === NonTokenizablePaymentMethods.accountCredit) {
|
||||
// Account credit doesn't need tokenization
|
||||
paymentMethodType = PaymentMethodType.Credit;
|
||||
paymentToken = "";
|
||||
} else {
|
||||
// Tokenize for card, bank account, or PayPal
|
||||
const paymentMethod = await this.enterPaymentMethodComponent.tokenize();
|
||||
paymentMethodType = tokenizablePaymentMethodToLegacyEnum(paymentMethod.type);
|
||||
paymentToken = paymentMethod.token;
|
||||
}
|
||||
const dialogRef = UnifiedUpgradeDialogComponent.open(this.dialogService, {
|
||||
data: dialogParams,
|
||||
});
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("paymentMethodType", paymentMethodType.toString());
|
||||
formData.append("paymentToken", paymentToken);
|
||||
formData.append(
|
||||
"additionalStorageGb",
|
||||
(this.formGroup.value.additionalStorage ?? 0).toString(),
|
||||
);
|
||||
formData.append("country", this.formGroup.value.billingAddress.country);
|
||||
formData.append("postalCode", this.formGroup.value.billingAddress.postalCode);
|
||||
|
||||
await this.apiService.postPremium(formData);
|
||||
await this.finalizeUpgrade();
|
||||
await this.postFinalizeUpgrade();
|
||||
};
|
||||
dialogRef.closed
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe((result: UnifiedUpgradeDialogResult | undefined) => {
|
||||
if (
|
||||
result?.status === UnifiedUpgradeDialogStatus.UpgradedToPremium ||
|
||||
result?.status === UnifiedUpgradeDialogStatus.UpgradedToFamilies
|
||||
) {
|
||||
void this.finalizeUpgrade();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,6 @@ export enum FeatureFlag {
|
||||
PM24032_NewNavigationPremiumUpgradeButton = "pm-24032-new-navigation-premium-upgrade-button",
|
||||
PM25379_UseNewOrganizationMetadataStructure = "pm-25379-use-new-organization-metadata-structure",
|
||||
PM24996_ImplementUpgradeFromFreeDialog = "pm-24996-implement-upgrade-from-free-dialog",
|
||||
PM24033PremiumUpgradeNewDesign = "pm-24033-updat-premium-subscription-page",
|
||||
PM26793_FetchPremiumPriceFromPricingService = "pm-26793-fetch-premium-price-from-pricing-service",
|
||||
PM23713_PremiumBadgeOpensNewPremiumUpgradeDialog = "pm-23713-premium-badge-opens-new-premium-upgrade-dialog",
|
||||
PM26462_Milestone_3 = "pm-26462-milestone-3",
|
||||
@@ -140,7 +139,6 @@ export const DefaultFeatureFlagValue = {
|
||||
[FeatureFlag.PM24032_NewNavigationPremiumUpgradeButton]: FALSE,
|
||||
[FeatureFlag.PM25379_UseNewOrganizationMetadataStructure]: FALSE,
|
||||
[FeatureFlag.PM24996_ImplementUpgradeFromFreeDialog]: FALSE,
|
||||
[FeatureFlag.PM24033PremiumUpgradeNewDesign]: FALSE,
|
||||
[FeatureFlag.PM26793_FetchPremiumPriceFromPricingService]: FALSE,
|
||||
[FeatureFlag.PM23713_PremiumBadgeOpensNewPremiumUpgradeDialog]: FALSE,
|
||||
[FeatureFlag.PM26462_Milestone_3]: FALSE,
|
||||
|
||||
Reference in New Issue
Block a user