mirror of
https://github.com/bitwarden/browser
synced 2026-02-05 19:23:19 +00:00
Move the changes to a new component
This commit is contained in:
@@ -1,11 +1,14 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
import { RouterModule, Routes } from "@angular/router";
|
||||
|
||||
import { featureFlaggedRoute } from "@bitwarden/angular/platform/utils/feature-flagged-route";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { AccountPaymentDetailsComponent } from "@bitwarden/web-vault/app/billing/individual/payment-details/account-payment-details.component";
|
||||
|
||||
import { PaymentMethodComponent } from "../shared";
|
||||
|
||||
import { BillingHistoryViewComponent } from "./billing-history-view.component";
|
||||
import { PremiumVNextComponent } from "./premium/premium-vnext.component";
|
||||
import { PremiumComponent } from "./premium/premium.component";
|
||||
import { SubscriptionComponent } from "./subscription.component";
|
||||
import { UserSubscriptionComponent } from "./user-subscription.component";
|
||||
@@ -22,11 +25,15 @@ const routes: Routes = [
|
||||
component: UserSubscriptionComponent,
|
||||
data: { titleId: "premiumMembership" },
|
||||
},
|
||||
{
|
||||
path: "premium",
|
||||
component: PremiumComponent,
|
||||
data: { titleId: "goPremium" },
|
||||
},
|
||||
...featureFlaggedRoute({
|
||||
defaultComponent: PremiumComponent,
|
||||
flaggedComponent: PremiumVNextComponent,
|
||||
featureFlag: FeatureFlag.PM24033PremiumUpgradeNewDesign,
|
||||
routeOptions: {
|
||||
data: { titleId: "goPremium" },
|
||||
path: "premium",
|
||||
},
|
||||
}),
|
||||
{
|
||||
path: "payment-method",
|
||||
component: PaymentMethodComponent,
|
||||
|
||||
@@ -3,10 +3,12 @@ import { NgModule } from "@angular/core";
|
||||
import { PricingCardComponent, CartSummaryComponent } from "@bitwarden/pricing";
|
||||
|
||||
import { HeaderModule } from "../../layouts/header/header.module";
|
||||
import { BillingServicesModule } from "../services";
|
||||
import { BillingSharedModule } from "../shared";
|
||||
|
||||
import { BillingHistoryViewComponent } from "./billing-history-view.component";
|
||||
import { IndividualBillingRoutingModule } from "./individual-billing-routing.module";
|
||||
import { PremiumVNextComponent } from "./premium/premium-vnext.component";
|
||||
import { PremiumComponent } from "./premium/premium.component";
|
||||
import { SubscriptionComponent } from "./subscription.component";
|
||||
import { UserSubscriptionComponent } from "./user-subscription.component";
|
||||
@@ -15,6 +17,7 @@ import { UserSubscriptionComponent } from "./user-subscription.component";
|
||||
imports: [
|
||||
IndividualBillingRoutingModule,
|
||||
BillingSharedModule,
|
||||
BillingServicesModule,
|
||||
HeaderModule,
|
||||
PricingCardComponent,
|
||||
CartSummaryComponent,
|
||||
@@ -24,6 +27,7 @@ import { UserSubscriptionComponent } from "./user-subscription.component";
|
||||
BillingHistoryViewComponent,
|
||||
UserSubscriptionComponent,
|
||||
PremiumComponent,
|
||||
PremiumVNextComponent,
|
||||
],
|
||||
})
|
||||
export class IndividualBillingModule {}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
<div class="tw-max-w-4xl tw-mx-auto">
|
||||
<bit-section *ngIf="shouldShowNewDesign$ | async">
|
||||
<div class="tw-max-w-4xl tw-ml-auto tw-mr-16 tw-text-center">
|
||||
<div
|
||||
class="tw-inline-block tw-bg-background-alt tw-border-[0.5px] tw-border-secondary-700 tw-rounded-full tw-px-2 tw-py-1 tw-mt-8 tw-mb-6"
|
||||
>
|
||||
<span bitTypography="helper" class="tw-text-secondary-700">
|
||||
{{ "bitwardenFreeplanMessage" | i18n }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h2 *ngIf="!isSelfHost" class="tw-mt-6 tw-text-4xl">
|
||||
{{ "upgradeCompleteSecurity" | i18n }}
|
||||
</h2>
|
||||
<p class="tw-text-muted tw-mb-6">
|
||||
{{ "unlockPremiumFeatures" | 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]="'planDescPremium' | 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: ('upgradeToFamilies' | 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">{{ "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>
|
||||
@@ -0,0 +1,183 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { combineLatest, firstValueFrom, map, Observable, shareReplay, switchMap } 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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
|
||||
import { SubscriptionPricingService } from "../../services/subscription-pricing.service";
|
||||
import { BitwardenSubscriber, mapAccountToSubscriber } from "../../types";
|
||||
import {
|
||||
PersonalSubscriptionPricingTier,
|
||||
PersonalSubscriptionPricingTierIds,
|
||||
} from "../../types/subscription-pricing-tier";
|
||||
import {
|
||||
UpgradePaymentDialogComponent,
|
||||
UpgradePaymentDialogResult,
|
||||
} from "../upgrade/upgrade-payment-dialog/upgrade-payment-dialog.component";
|
||||
|
||||
@Component({
|
||||
templateUrl: "./premium-vnext.component.html",
|
||||
standalone: false,
|
||||
})
|
||||
export class PremiumVNextComponent {
|
||||
protected hasPremiumFromAnyOrganization$: Observable<boolean>;
|
||||
protected hasPremiumPersonally$: Observable<boolean>;
|
||||
protected shouldShowNewDesign$: 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;
|
||||
protected isSelfHost = false;
|
||||
|
||||
constructor(
|
||||
private accountService: AccountService,
|
||||
private i18nService: I18nService,
|
||||
private apiService: ApiService,
|
||||
private dialogService: DialogService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private syncService: SyncService,
|
||||
private toastService: ToastService,
|
||||
private billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||
private subscriptionPricingService: SubscriptionPricingService,
|
||||
) {
|
||||
this.isSelfHost = this.platformUtilsService.isSelfHost();
|
||||
|
||||
this.hasPremiumFromAnyOrganization$ = this.accountService.activeAccount$.pipe(
|
||||
switchMap((account) =>
|
||||
this.billingAccountProfileStateService.hasPremiumFromAnyOrganization$(account.id),
|
||||
),
|
||||
);
|
||||
|
||||
this.hasPremiumPersonally$ = this.accountService.activeAccount$.pipe(
|
||||
switchMap((account) =>
|
||||
this.billingAccountProfileStateService.hasPremiumPersonally$(account.id),
|
||||
),
|
||||
);
|
||||
|
||||
this.accountService.activeAccount$
|
||||
.pipe(mapAccountToSubscriber, takeUntilDestroyed())
|
||||
.subscribe((subscriber) => {
|
||||
this.subscriber = subscriber;
|
||||
});
|
||||
|
||||
this.shouldShowNewDesign$ = combineLatest([
|
||||
this.hasPremiumFromAnyOrganization$,
|
||||
this.hasPremiumPersonally$,
|
||||
]).pipe(map(([hasOrgPremium, hasPersonalPremium]) => !hasOrgPremium && !hasPersonalPremium));
|
||||
|
||||
this.personalPricingTiers$ =
|
||||
this.subscriptionPricingService.getPersonalSubscriptionPricingTiers$();
|
||||
|
||||
this.premiumCardData$ = this.personalPricingTiers$.pipe(
|
||||
map((tiers) => {
|
||||
const tier = tiers.find((t) => t.id === "premium");
|
||||
return {
|
||||
tier,
|
||||
price:
|
||||
tier?.passwordManager.type === "standalone"
|
||||
? 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 === "families");
|
||||
return {
|
||||
tier,
|
||||
price:
|
||||
tier?.passwordManager.type === "packaged"
|
||||
? Number((tier.passwordManager.annualPrice / 12).toFixed(2))
|
||||
: 0,
|
||||
features: tier?.passwordManager.features.map((f) => f.value) || [],
|
||||
};
|
||||
}),
|
||||
shareReplay({ bufferSize: 1, refCount: true }),
|
||||
);
|
||||
}
|
||||
|
||||
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"),
|
||||
});
|
||||
};
|
||||
|
||||
protected async openUpgradeDialog(type: "Premium" | "Families"): Promise<void> {
|
||||
try {
|
||||
const planId =
|
||||
type === "Premium"
|
||||
? PersonalSubscriptionPricingTierIds.Premium
|
||||
: PersonalSubscriptionPricingTierIds.Families;
|
||||
|
||||
if (!this.subscriber) {
|
||||
throw new Error("No subscriber found");
|
||||
}
|
||||
|
||||
const paymentDialogRef = UpgradePaymentDialogComponent.open(this.dialogService, {
|
||||
data: {
|
||||
plan: planId,
|
||||
subscriber: this.subscriber,
|
||||
},
|
||||
});
|
||||
|
||||
const paymentResult = await firstValueFrom(paymentDialogRef.closed);
|
||||
await this.handleUpgradeResult(paymentResult, planId);
|
||||
} catch {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("errorOccurred"),
|
||||
message: this.i18nService.t("unexpectedError"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async handleUpgradeResult(
|
||||
result: UpgradePaymentDialogResult | null,
|
||||
plan: string,
|
||||
): Promise<void> {
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (result) {
|
||||
case "upgradedToPremium":
|
||||
await this.finalizeUpgrade();
|
||||
await this.postFinalizeUpgrade();
|
||||
break;
|
||||
case "upgradedToFamilies":
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: this.i18nService.t("familiesUpgradeSuccess"),
|
||||
});
|
||||
break;
|
||||
case "back":
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,191 +1,119 @@
|
||||
<div class="tw-max-w-4xl tw-mx-auto">
|
||||
<bit-section *ngIf="shouldShowNewDesign$ | async">
|
||||
<div class="tw-max-w-4xl tw-ml-auto tw-mr-16 tw-text-center">
|
||||
<div
|
||||
class="tw-inline-block tw-bg-background-alt tw-border-[0.5px] tw-border-secondary-700 tw-rounded-full tw-px-2 tw-py-1 tw-mt-8 tw-mb-6"
|
||||
>
|
||||
<span bitTypography="helper" class="tw-text-secondary-700">
|
||||
{{ "bitwardenFreeplanMessage" | i18n }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h2 *ngIf="!isSelfHost" class="tw-mt-6 tw-text-4xl">
|
||||
{{ "upgradeCompleteSecurity" | i18n }}
|
||||
</h2>
|
||||
<p class="tw-text-muted tw-mb-6">
|
||||
{{ "unlockPremiumFeatures" | 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]="'planDescPremium' | 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: ('upgradeToFamilies' | 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">{{ "individualUpgradeTaxInformationMessage" | i18n }}</p>
|
||||
<bit-section>
|
||||
<h2 *ngIf="!isSelfHost" bitTypography="h2">{{ "goPremium" | i18n }}</h2>
|
||||
<bit-callout
|
||||
type="info"
|
||||
*ngIf="hasPremiumFromAnyOrganization$ | async"
|
||||
title="{{ 'youHavePremiumAccess' | i18n }}"
|
||||
icon="bwi bwi-star-f"
|
||||
>
|
||||
{{ "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>
|
||||
{{ "premiumSignUpStorage" | i18n }}
|
||||
</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" [ngClass]="{ 'tw-mb-0': !isSelfHost }">
|
||||
{{
|
||||
"premiumPriceWithFamilyPlan" | i18n: (premiumPrice | currency: "$") : familyPlanMaxUserCount
|
||||
}}
|
||||
<a
|
||||
bitLink
|
||||
linkType="primary"
|
||||
href="https://bitwarden.com/pricing/business/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
routerLink="/create-organization"
|
||||
[queryParams]="{ plan: 'families' }"
|
||||
>
|
||||
{{ "viewbusinessplans" | i18n }}
|
||||
<i class="bwi bwi-external-link tw-ml-1" aria-hidden="true"></i>
|
||||
{{ "bitwardenFamiliesPlan" | i18n }}
|
||||
</a>
|
||||
</p>
|
||||
<a
|
||||
bitButton
|
||||
href="{{ premiumURL }}"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
buttonType="secondary"
|
||||
*ngIf="isSelfHost"
|
||||
>
|
||||
{{ "purchasePremium" | i18n }}
|
||||
</a>
|
||||
</bit-callout>
|
||||
</bit-section>
|
||||
<bit-section *ngIf="isSelfHost">
|
||||
<individual-self-hosting-license-uploader
|
||||
(onLicenseFileUploaded)="onLicenseFileSelectedChanged()"
|
||||
/>
|
||||
</bit-section>
|
||||
<form *ngIf="!isSelfHost" [formGroup]="addOnFormGroup" [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: "1 GB" : (storageGBPrice | currency: "$") : ("year" | i18n)
|
||||
}}</bit-hint>
|
||||
</bit-form-field>
|
||||
</div>
|
||||
</bit-section>
|
||||
</div>
|
||||
<!-- Legacy Design (shown when user already has premium access) -->
|
||||
<div *ngIf="!(shouldShowNewDesign$ | async)">
|
||||
<bit-section>
|
||||
<h2 *ngIf="!isSelfHost" bitTypography="h2">{{ "goPremium" | i18n }}</h2>
|
||||
<bit-callout
|
||||
type="info"
|
||||
*ngIf="hasPremiumFromAnyOrganization$ | async"
|
||||
title="{{ 'youHavePremiumAccess' | i18n }}"
|
||||
icon="bwi bwi-star-f"
|
||||
>
|
||||
{{ "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>
|
||||
{{ "premiumSignUpStorage" | i18n }}
|
||||
</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" [ngClass]="{ 'tw-mb-0': !isSelfHost }">
|
||||
{{
|
||||
"premiumPriceWithFamilyPlan"
|
||||
| i18n: (premiumPrice | currency: "$") : familyPlanMaxUserCount
|
||||
}}
|
||||
<a
|
||||
bitLink
|
||||
linkType="primary"
|
||||
routerLink="/create-organization"
|
||||
[queryParams]="{ plan: 'families' }"
|
||||
>
|
||||
{{ "bitwardenFamiliesPlan" | i18n }}
|
||||
</a>
|
||||
</p>
|
||||
<a
|
||||
bitButton
|
||||
href="{{ premiumURL }}"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
buttonType="secondary"
|
||||
*ngIf="isSelfHost"
|
||||
>
|
||||
{{ "purchasePremium" | i18n }}
|
||||
</a>
|
||||
</bit-callout>
|
||||
<h2 bitTypography="h2">{{ "summary" | i18n }}</h2>
|
||||
{{ "premiumMembership" | i18n }}: {{ premiumPrice | currency: "$" }} <br />
|
||||
{{ "additionalStorageGb" | i18n }}: {{ addOnFormGroup.value.additionalStorage || 0 }} GB ×
|
||||
{{ storageGBPrice | currency: "$" }} =
|
||||
{{ additionalStorageCost | currency: "$" }}
|
||||
<hr class="tw-my-3" />
|
||||
</bit-section>
|
||||
<bit-section *ngIf="isSelfHost">
|
||||
<individual-self-hosting-license-uploader
|
||||
(onLicenseFileUploaded)="onLicenseFileSelectedChanged()"
|
||||
/>
|
||||
<bit-section>
|
||||
<h3 bitTypography="h2">{{ "paymentInformation" | i18n }}</h3>
|
||||
<app-payment [showBankAccount]="false"></app-payment>
|
||||
<app-tax-info (taxInformationChanged)="onTaxInformationChanged()"></app-tax-info>
|
||||
<div class="tw-mb-4">
|
||||
<div class="tw-text-muted tw-text-sm tw-flex tw-flex-col">
|
||||
<span>{{ "planPrice" | i18n }}: {{ subtotal | currency: "USD $" }}</span>
|
||||
<span>{{ "estimatedTax" | i18n }}: {{ estimatedTax | 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 | currency: "USD $" }}/{{ "year" | i18n }}
|
||||
</p>
|
||||
<button type="submit" buttonType="primary" bitButton bitFormButton>
|
||||
{{ "submit" | i18n }}
|
||||
</button>
|
||||
</bit-section>
|
||||
<form *ngIf="!isSelfHost" [formGroup]="addOnFormGroup" [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: "1 GB" : (storageGBPrice | currency: "$") : ("year" | i18n)
|
||||
}}</bit-hint>
|
||||
</bit-form-field>
|
||||
</div>
|
||||
</bit-section>
|
||||
<bit-section>
|
||||
<h2 bitTypography="h2">{{ "summary" | i18n }}</h2>
|
||||
{{ "premiumMembership" | i18n }}: {{ premiumPrice | currency: "$" }} <br />
|
||||
{{ "additionalStorageGb" | i18n }}: {{ addOnFormGroup.value.additionalStorage || 0 }} GB
|
||||
× {{ storageGBPrice | currency: "$" }} =
|
||||
{{ additionalStorageCost | currency: "$" }}
|
||||
<hr class="tw-my-3" />
|
||||
</bit-section>
|
||||
<bit-section>
|
||||
<h3 bitTypography="h2">{{ "paymentInformation" | i18n }}</h3>
|
||||
<app-payment [showBankAccount]="false"></app-payment>
|
||||
<app-tax-info (taxInformationChanged)="onTaxInformationChanged()"></app-tax-info>
|
||||
<div class="tw-mb-4">
|
||||
<div class="tw-text-muted tw-text-sm tw-flex tw-flex-col">
|
||||
<span>{{ "planPrice" | i18n }}: {{ subtotal | currency: "USD $" }}</span>
|
||||
<span>{{ "estimatedTax" | i18n }}: {{ estimatedTax | 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 | currency: "USD $" }}/{{ "year" | i18n }}
|
||||
</p>
|
||||
<button type="submit" buttonType="primary" bitButton bitFormButton>
|
||||
{{ "submit" | i18n }}
|
||||
</button>
|
||||
</bit-section>
|
||||
</form>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -4,8 +4,8 @@ import { Component, ViewChild } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { FormControl, FormGroup, Validators } from "@angular/forms";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { combineLatest, concatMap, firstValueFrom, from, Observable, of, switchMap } from "rxjs";
|
||||
import { debounceTime, map, shareReplay } from "rxjs/operators";
|
||||
import { combineLatest, concatMap, from, Observable, of, switchMap } from "rxjs";
|
||||
import { debounceTime } from "rxjs/operators";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
@@ -13,26 +13,15 @@ import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
|
||||
import { TaxServiceAbstraction } from "@bitwarden/common/billing/abstractions/tax.service.abstraction";
|
||||
import { PreviewIndividualInvoiceRequest } from "@bitwarden/common/billing/models/request/preview-individual-invoice.request";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
|
||||
import { PaymentComponent } from "../../shared/payment/payment.component";
|
||||
import { TaxInfoComponent } from "../../shared/tax-info.component";
|
||||
import {
|
||||
PersonalSubscriptionPricingTier,
|
||||
PersonalSubscriptionPricingTierIds,
|
||||
} from "../../types/subscription-pricing-tier";
|
||||
import { BitwardenSubscriber, mapAccountToSubscriber } from "../../types/bitwarden-subscriber";
|
||||
import { SubscriptionPricingService } from "../../services/subscription-pricing.service";
|
||||
import {
|
||||
UpgradePaymentDialogComponent,
|
||||
UpgradePaymentDialogResult,
|
||||
} from "../upgrade/upgrade-payment-dialog/upgrade-payment-dialog.component";
|
||||
|
||||
@Component({
|
||||
templateUrl: "./premium.component.html",
|
||||
@@ -43,19 +32,6 @@ export class PremiumComponent {
|
||||
@ViewChild(TaxInfoComponent) taxInfoComponent: TaxInfoComponent;
|
||||
|
||||
protected hasPremiumFromAnyOrganization$: Observable<boolean>;
|
||||
protected hasPremiumPersonally$: Observable<boolean>;
|
||||
protected shouldShowNewDesign$: 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 addOnFormGroup = new FormGroup({
|
||||
additionalStorage: new FormControl<number>(0, [Validators.min(0), Validators.max(99)]),
|
||||
@@ -67,7 +43,6 @@ export class PremiumComponent {
|
||||
|
||||
protected cloudWebVaultURL: string;
|
||||
protected isSelfHost = false;
|
||||
protected subscriber: BitwardenSubscriber;
|
||||
|
||||
protected estimatedTax: number = 0;
|
||||
protected readonly familyPlanMaxUserCount = 6;
|
||||
@@ -88,8 +63,6 @@ export class PremiumComponent {
|
||||
private tokenService: TokenService,
|
||||
private taxService: TaxServiceAbstraction,
|
||||
private accountService: AccountService,
|
||||
private dialogService: DialogService,
|
||||
private subscriptionPricingService: SubscriptionPricingService,
|
||||
) {
|
||||
this.isSelfHost = this.platformUtilsService.isSelfHost();
|
||||
|
||||
@@ -99,62 +72,6 @@ export class PremiumComponent {
|
||||
),
|
||||
);
|
||||
|
||||
this.hasPremiumPersonally$ = this.accountService.activeAccount$.pipe(
|
||||
switchMap((account) =>
|
||||
this.billingAccountProfileStateService.hasPremiumPersonally$(account.id),
|
||||
),
|
||||
);
|
||||
|
||||
this.accountService.activeAccount$
|
||||
.pipe(mapAccountToSubscriber, takeUntilDestroyed())
|
||||
.subscribe((subscriber) => {
|
||||
this.subscriber = subscriber;
|
||||
});
|
||||
|
||||
this.shouldShowNewDesign$ = combineLatest([
|
||||
this.hasPremiumFromAnyOrganization$,
|
||||
this.hasPremiumPersonally$,
|
||||
this.configService.getFeatureFlag$(FeatureFlag.PremiumUpgradeNewDesign),
|
||||
]).pipe(
|
||||
map(
|
||||
([hasOrgPremium, hasPersonalPremium, isNewDesignEnabled]) =>
|
||||
isNewDesignEnabled && !hasOrgPremium && !hasPersonalPremium,
|
||||
),
|
||||
);
|
||||
|
||||
this.personalPricingTiers$ =
|
||||
this.subscriptionPricingService.getPersonalSubscriptionPricingTiers$();
|
||||
|
||||
this.premiumCardData$ = this.personalPricingTiers$.pipe(
|
||||
map((tiers) => {
|
||||
const tier = tiers.find((t) => t.id === "premium");
|
||||
return {
|
||||
tier,
|
||||
price:
|
||||
tier?.passwordManager.type === "standalone"
|
||||
? 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 === "families");
|
||||
return {
|
||||
tier,
|
||||
price:
|
||||
tier?.passwordManager.type === "packaged"
|
||||
? Number((tier.passwordManager.annualPrice / 12).toFixed(2))
|
||||
: 0,
|
||||
features: tier?.passwordManager.features.map((f) => f.value) || [],
|
||||
};
|
||||
}),
|
||||
shareReplay({ bufferSize: 1, refCount: true }),
|
||||
);
|
||||
|
||||
combineLatest([
|
||||
this.accountService.activeAccount$.pipe(
|
||||
switchMap((account) =>
|
||||
@@ -304,61 +221,4 @@ export class PremiumComponent {
|
||||
protected onTaxInformationChanged(): void {
|
||||
this.refreshSalesTax();
|
||||
}
|
||||
|
||||
protected async openUpgradeDialog(type: "Premium" | "Families"): Promise<void> {
|
||||
try {
|
||||
const planId =
|
||||
type === "Premium"
|
||||
? PersonalSubscriptionPricingTierIds.Premium
|
||||
: PersonalSubscriptionPricingTierIds.Families;
|
||||
|
||||
if (!this.subscriber) {
|
||||
throw new Error("No subscriber found");
|
||||
}
|
||||
|
||||
const paymentDialogRef = UpgradePaymentDialogComponent.open(this.dialogService, {
|
||||
data: {
|
||||
plan: planId,
|
||||
subscriber: this.subscriber,
|
||||
},
|
||||
});
|
||||
|
||||
const paymentResult = await firstValueFrom(paymentDialogRef.closed);
|
||||
await this.handleUpgradeResult(paymentResult, planId);
|
||||
} catch (error) {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("errorOccurred"),
|
||||
message: this.i18nService.t("unexpectedError"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async handleUpgradeResult(
|
||||
result: UpgradePaymentDialogResult | null,
|
||||
plan: string,
|
||||
): Promise<void> {
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (result) {
|
||||
case "upgradedToPremium":
|
||||
await this.finalizeUpgrade();
|
||||
await this.postFinalizeUpgrade();
|
||||
break;
|
||||
case "upgradedToFamilies":
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: this.i18nService.t("familiesUpgradeSuccess"),
|
||||
});
|
||||
// Navigate to the organizations page after successful families upgrade
|
||||
// Note: We would need the organization ID from the upgrade service to navigate properly
|
||||
break;
|
||||
case "back":
|
||||
// User went back, could re-open the account dialog
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ export enum FeatureFlag {
|
||||
PM21881_ManagePaymentDetailsOutsideCheckout = "pm-21881-manage-payment-details-outside-checkout",
|
||||
PM21821_ProviderPortalTakeover = "pm-21821-provider-portal-takeover",
|
||||
PM22415_TaxIDWarnings = "pm-22415-tax-id-warnings",
|
||||
PremiumUpgradeNewDesign = "pm-24033-updat-premium-subscription-page",
|
||||
PM24033PremiumUpgradeNewDesign = "pm-24033-update-premium-subscription-page",
|
||||
|
||||
/* Key Management */
|
||||
PrivateKeyRegeneration = "pm-12241-private-key-regeneration",
|
||||
@@ -108,7 +108,7 @@ export const DefaultFeatureFlagValue = {
|
||||
[FeatureFlag.PM21881_ManagePaymentDetailsOutsideCheckout]: FALSE,
|
||||
[FeatureFlag.PM21821_ProviderPortalTakeover]: FALSE,
|
||||
[FeatureFlag.PM22415_TaxIDWarnings]: FALSE,
|
||||
[FeatureFlag.PremiumUpgradeNewDesign]: FALSE,
|
||||
[FeatureFlag.PM24033PremiumUpgradeNewDesign]: FALSE,
|
||||
|
||||
/* Key Management */
|
||||
[FeatureFlag.PrivateKeyRegeneration]: FALSE,
|
||||
|
||||
Reference in New Issue
Block a user