mirror of
https://github.com/bitwarden/browser
synced 2025-12-12 22:33:35 +00:00
[PM-24033]Implement Subscription Settings UI with Premium and Families Cards (#16822)
* Add initial changes for thenew premium design * Add the messages * Add the new dialog modal * Resolve the flag issue * Added changes for redirect * Fix the unitest errors * Resolve the badge issue * refactor the code base pr comments
This commit is contained in:
@@ -1,9 +1,12 @@
|
|||||||
import { NgModule } from "@angular/core";
|
import { NgModule } from "@angular/core";
|
||||||
import { RouterModule, Routes } from "@angular/router";
|
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 { AccountPaymentDetailsComponent } from "@bitwarden/web-vault/app/billing/individual/payment-details/account-payment-details.component";
|
||||||
|
|
||||||
import { BillingHistoryViewComponent } from "./billing-history-view.component";
|
import { BillingHistoryViewComponent } from "./billing-history-view.component";
|
||||||
|
import { PremiumVNextComponent } from "./premium/premium-vnext.component";
|
||||||
import { PremiumComponent } from "./premium/premium.component";
|
import { PremiumComponent } from "./premium/premium.component";
|
||||||
import { SubscriptionComponent } from "./subscription.component";
|
import { SubscriptionComponent } from "./subscription.component";
|
||||||
import { UserSubscriptionComponent } from "./user-subscription.component";
|
import { UserSubscriptionComponent } from "./user-subscription.component";
|
||||||
@@ -20,11 +23,15 @@ const routes: Routes = [
|
|||||||
component: UserSubscriptionComponent,
|
component: UserSubscriptionComponent,
|
||||||
data: { titleId: "premiumMembership" },
|
data: { titleId: "premiumMembership" },
|
||||||
},
|
},
|
||||||
{
|
...featureFlaggedRoute({
|
||||||
path: "premium",
|
defaultComponent: PremiumComponent,
|
||||||
component: PremiumComponent,
|
flaggedComponent: PremiumVNextComponent,
|
||||||
data: { titleId: "goPremium" },
|
featureFlag: FeatureFlag.PM24033PremiumUpgradeNewDesign,
|
||||||
},
|
routeOptions: {
|
||||||
|
data: { titleId: "goPremium" },
|
||||||
|
path: "premium",
|
||||||
|
},
|
||||||
|
}),
|
||||||
{
|
{
|
||||||
path: "payment-details",
|
path: "payment-details",
|
||||||
component: AccountPaymentDetailsComponent,
|
component: AccountPaymentDetailsComponent,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { NgModule } from "@angular/core";
|
import { NgModule } from "@angular/core";
|
||||||
|
|
||||||
|
import { PricingCardComponent } from "@bitwarden/pricing";
|
||||||
import {
|
import {
|
||||||
EnterBillingAddressComponent,
|
EnterBillingAddressComponent,
|
||||||
EnterPaymentMethodComponent,
|
EnterPaymentMethodComponent,
|
||||||
@@ -21,6 +22,7 @@ import { UserSubscriptionComponent } from "./user-subscription.component";
|
|||||||
HeaderModule,
|
HeaderModule,
|
||||||
EnterPaymentMethodComponent,
|
EnterPaymentMethodComponent,
|
||||||
EnterBillingAddressComponent,
|
EnterBillingAddressComponent,
|
||||||
|
PricingCardComponent,
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
SubscriptionComponent,
|
SubscriptionComponent,
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
<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 *ngIf="!isSelfHost" 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]="'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 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>
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
import { CommonModule } from "@angular/common";
|
||||||
|
import { Component, DestroyRef, inject } from "@angular/core";
|
||||||
|
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||||
|
import { combineLatest, firstValueFrom, map, Observable, of, 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,
|
||||||
|
SectionComponent,
|
||||||
|
BadgeModule,
|
||||||
|
TypographyModule,
|
||||||
|
LinkModule,
|
||||||
|
} from "@bitwarden/components";
|
||||||
|
import { PricingCardComponent } from "@bitwarden/pricing";
|
||||||
|
import { I18nPipe } from "@bitwarden/ui-common";
|
||||||
|
|
||||||
|
import { SubscriptionPricingService } from "../../services/subscription-pricing.service";
|
||||||
|
import { BitwardenSubscriber, mapAccountToSubscriber } from "../../types";
|
||||||
|
import {
|
||||||
|
PersonalSubscriptionPricingTier,
|
||||||
|
PersonalSubscriptionPricingTierIds,
|
||||||
|
} from "../../types/subscription-pricing-tier";
|
||||||
|
import {
|
||||||
|
UnifiedUpgradeDialogComponent,
|
||||||
|
UnifiedUpgradeDialogParams,
|
||||||
|
UnifiedUpgradeDialogResult,
|
||||||
|
UnifiedUpgradeDialogStatus,
|
||||||
|
UnifiedUpgradeDialogStep,
|
||||||
|
} from "../upgrade/unified-upgrade-dialog/unified-upgrade-dialog.component";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
templateUrl: "./premium-vnext.component.html",
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
SectionComponent,
|
||||||
|
BadgeModule,
|
||||||
|
TypographyModule,
|
||||||
|
LinkModule,
|
||||||
|
I18nPipe,
|
||||||
|
PricingCardComponent,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
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;
|
||||||
|
private destroyRef = inject(DestroyRef);
|
||||||
|
|
||||||
|
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) =>
|
||||||
|
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));
|
||||||
|
|
||||||
|
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"
|
||||||
|
? 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"
|
||||||
|
? 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);
|
||||||
|
};
|
||||||
|
|
||||||
|
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,137 +1,132 @@
|
|||||||
<bit-section>
|
<bit-container>
|
||||||
<h2 *ngIf="!isSelfHost" bitTypography="h2">{{ "goPremium" | i18n }}</h2>
|
<bit-section>
|
||||||
<bit-callout
|
<h2 *ngIf="!isSelfHost" bitTypography="h2">{{ "goPremium" | i18n }}</h2>
|
||||||
type="info"
|
<bit-callout
|
||||||
*ngIf="hasPremiumFromAnyOrganization$ | async"
|
type="info"
|
||||||
title="{{ 'youHavePremiumAccess' | i18n }}"
|
*ngIf="hasPremiumFromAnyOrganization$ | async"
|
||||||
icon="bwi bwi-star-f"
|
title="{{ 'youHavePremiumAccess' | i18n }}"
|
||||||
>
|
icon="bwi bwi-star-f"
|
||||||
{{ "alreadyPremiumFromOrg" | i18n }}
|
>
|
||||||
</bit-callout>
|
{{ "alreadyPremiumFromOrg" | i18n }}
|
||||||
<bit-callout type="success">
|
</bit-callout>
|
||||||
<p>{{ "premiumUpgradeUnlockFeatures" | i18n }}</p>
|
<bit-callout type="success">
|
||||||
<ul class="bwi-ul">
|
<p>{{ "premiumUpgradeUnlockFeatures" | i18n }}</p>
|
||||||
<li>
|
<ul class="bwi-ul">
|
||||||
<i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i>
|
<li>
|
||||||
{{ "premiumSignUpStorage" | i18n }}
|
<i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i>
|
||||||
</li>
|
{{ "premiumSignUpStorage" | i18n }}
|
||||||
<li>
|
</li>
|
||||||
<i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i>
|
<li>
|
||||||
{{ "premiumSignUpTwoStepOptions" | i18n }}
|
<i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i>
|
||||||
</li>
|
{{ "premiumSignUpTwoStepOptions" | i18n }}
|
||||||
<li>
|
</li>
|
||||||
<i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i>
|
<li>
|
||||||
{{ "premiumSignUpEmergency" | i18n }}
|
<i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i>
|
||||||
</li>
|
{{ "premiumSignUpEmergency" | i18n }}
|
||||||
<li>
|
</li>
|
||||||
<i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i>
|
<li>
|
||||||
{{ "premiumSignUpReports" | i18n }}
|
<i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i>
|
||||||
</li>
|
{{ "premiumSignUpReports" | i18n }}
|
||||||
<li>
|
</li>
|
||||||
<i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i>
|
<li>
|
||||||
{{ "premiumSignUpTotp" | i18n }}
|
<i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i>
|
||||||
</li>
|
{{ "premiumSignUpTotp" | i18n }}
|
||||||
<li>
|
</li>
|
||||||
<i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i>
|
<li>
|
||||||
{{ "premiumSignUpSupport" | i18n }}
|
<i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i>
|
||||||
</li>
|
{{ "premiumSignUpSupport" | i18n }}
|
||||||
<li>
|
</li>
|
||||||
<i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i>
|
<li>
|
||||||
{{ "premiumSignUpFuture" | i18n }}
|
<i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i>
|
||||||
</li>
|
{{ "premiumSignUpFuture" | i18n }}
|
||||||
</ul>
|
</li>
|
||||||
<p bitTypography="body1" [ngClass]="{ 'tw-mb-0': !isSelfHost }">
|
</ul>
|
||||||
{{
|
<p bitTypography="body1" [ngClass]="{ 'tw-mb-0': !isSelfHost }">
|
||||||
"premiumPriceWithFamilyPlan" | i18n: (premiumPrice | currency: "$") : familyPlanMaxUserCount
|
{{
|
||||||
}}
|
"premiumPriceWithFamilyPlan"
|
||||||
|
| i18n: (premiumPrice | currency: "$") : familyPlanMaxUserCount
|
||||||
|
}}
|
||||||
|
<a
|
||||||
|
bitLink
|
||||||
|
linkType="primary"
|
||||||
|
routerLink="/create-organization"
|
||||||
|
[queryParams]="{ plan: 'families' }"
|
||||||
|
>
|
||||||
|
{{ "bitwardenFamiliesPlan" | i18n }}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
<a
|
<a
|
||||||
bitLink
|
bitButton
|
||||||
linkType="primary"
|
href="{{ premiumURL }}"
|
||||||
routerLink="/create-organization"
|
target="_blank"
|
||||||
[queryParams]="{ plan: 'families' }"
|
rel="noreferrer"
|
||||||
|
buttonType="secondary"
|
||||||
|
*ngIf="isSelfHost"
|
||||||
>
|
>
|
||||||
{{ "bitwardenFamiliesPlan" | i18n }}
|
{{ "purchasePremium" | i18n }}
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</bit-callout>
|
||||||
<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]="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: "1 GB" : (storageGBPrice | currency: "$") : ("year" | i18n)
|
|
||||||
}}</bit-hint>
|
|
||||||
</bit-form-field>
|
|
||||||
</div>
|
|
||||||
</bit-section>
|
</bit-section>
|
||||||
<bit-section>
|
<bit-section *ngIf="isSelfHost">
|
||||||
<h2 bitTypography="h2">{{ "summary" | i18n }}</h2>
|
<individual-self-hosting-license-uploader
|
||||||
{{ "premiumMembership" | i18n }}: {{ premiumPrice | currency: "$" }} <br />
|
(onLicenseFileUploaded)="onLicenseFileSelectedChanged()"
|
||||||
{{ "additionalStorageGb" | i18n }}: {{ formGroup.value.additionalStorage || 0 }} GB ×
|
/>
|
||||||
{{ storageGBPrice | currency: "$" }} =
|
|
||||||
{{ additionalStorageCost | currency: "$" }}
|
|
||||||
<hr class="tw-my-3" />
|
|
||||||
</bit-section>
|
</bit-section>
|
||||||
<bit-section>
|
<form *ngIf="!isSelfHost" [formGroup]="formGroup" [bitSubmit]="submitPayment">
|
||||||
<h3 bitTypography="h2">{{ "paymentInformation" | i18n }}</h3>
|
<bit-section>
|
||||||
<div class="tw-mb-4">
|
<h2 bitTypography="h2">{{ "addons" | i18n }}</h2>
|
||||||
<app-enter-payment-method
|
<div class="tw-grid tw-grid-cols-12 tw-gap-4">
|
||||||
[group]="formGroup.controls.paymentMethod"
|
<bit-form-field class="tw-col-span-6">
|
||||||
[showBankAccount]="false"
|
<bit-label>{{ "additionalStorageGb" | i18n }}</bit-label>
|
||||||
[showAccountCredit]="true"
|
<input
|
||||||
[hasEnoughAccountCredit]="hasEnoughAccountCredit$ | async"
|
bitInput
|
||||||
>
|
formControlName="additionalStorage"
|
||||||
</app-enter-payment-method>
|
type="number"
|
||||||
<app-enter-billing-address
|
step="1"
|
||||||
[group]="formGroup.controls.billingAddress"
|
placeholder="{{ 'additionalStorageGbDesc' | i18n }}"
|
||||||
[scenario]="{ type: 'checkout', supportsTaxId: false }"
|
/>
|
||||||
>
|
<bit-hint>{{
|
||||||
</app-enter-billing-address>
|
"additionalStorageIntervalDesc"
|
||||||
</div>
|
| i18n: "1 GB" : (storageGBPrice | currency: "$") : ("year" | i18n)
|
||||||
<div class="tw-mb-4">
|
}}</bit-hint>
|
||||||
<div class="tw-text-muted tw-text-sm tw-flex tw-flex-col">
|
</bit-form-field>
|
||||||
<span>{{ "planPrice" | i18n }}: {{ subtotal | currency: "USD $" }}</span>
|
|
||||||
<span>{{ "estimatedTax" | i18n }}: {{ estimatedTax | currency: "USD $" }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</bit-section>
|
||||||
<hr class="tw-my-1 tw-w-1/4 tw-ml-0" />
|
<bit-section>
|
||||||
<p bitTypography="body1">
|
<h2 bitTypography="h2">{{ "summary" | i18n }}</h2>
|
||||||
<strong>{{ "total" | i18n }}:</strong> {{ total | currency: "USD $" }}/{{ "year" | i18n }}
|
{{ "premiumMembership" | i18n }}: {{ premiumPrice | currency: "$" }} <br />
|
||||||
</p>
|
{{ "additionalStorageGb" | i18n }}: {{ formGroup.value.additionalStorage || 0 }} GB ×
|
||||||
<button
|
{{ storageGBPrice | currency: "$" }} =
|
||||||
type="submit"
|
{{ additionalStorageCost | currency: "$" }}
|
||||||
buttonType="primary"
|
<hr class="tw-my-3" />
|
||||||
bitButton
|
</bit-section>
|
||||||
bitFormButton
|
<bit-section>
|
||||||
[disabled]="!(hasEnoughAccountCredit$ | async)"
|
<h3 bitTypography="h2">{{ "paymentInformation" | i18n }}</h3>
|
||||||
>
|
<div class="tw-mb-4">
|
||||||
{{ "submit" | i18n }}
|
<app-enter-payment-method
|
||||||
</button>
|
[group]="formGroup.controls.paymentMethod"
|
||||||
</bit-section>
|
[showBankAccount]="false"
|
||||||
</form>
|
>
|
||||||
|
</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 | 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>
|
||||||
|
</bit-container>
|
||||||
|
|||||||
@@ -8,6 +8,4 @@
|
|||||||
</bit-tab-nav-bar>
|
</bit-tab-nav-bar>
|
||||||
</app-header>
|
</app-header>
|
||||||
|
|
||||||
<bit-container>
|
<router-outlet></router-outlet>
|
||||||
<router-outlet></router-outlet>
|
|
||||||
</bit-container>
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { DIALOG_DATA } from "@angular/cdk/dialog";
|
import { DIALOG_DATA } from "@angular/cdk/dialog";
|
||||||
import { CommonModule } from "@angular/common";
|
import { CommonModule } from "@angular/common";
|
||||||
import { Component, Inject, OnInit, signal } from "@angular/core";
|
import { Component, Inject, OnInit, signal } from "@angular/core";
|
||||||
|
import { Router } from "@angular/router";
|
||||||
|
|
||||||
import { Account } from "@bitwarden/common/auth/abstractions/account.service";
|
import { Account } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { UnionOfValues } from "@bitwarden/common/vault/types/union-of-values";
|
import { UnionOfValues } from "@bitwarden/common/vault/types/union-of-values";
|
||||||
@@ -50,6 +51,7 @@ export type UnifiedUpgradeDialogResult = {
|
|||||||
* @property {PersonalSubscriptionPricingTierId | null} [selectedPlan] - Pre-selected subscription plan, if any.
|
* @property {PersonalSubscriptionPricingTierId | null} [selectedPlan] - Pre-selected subscription plan, if any.
|
||||||
* @property {string | null} [dialogTitleMessageOverride] - Optional custom i18n key to override the default dialog title.
|
* @property {string | null} [dialogTitleMessageOverride] - Optional custom i18n key to override the default dialog title.
|
||||||
* @property {boolean} [hideContinueWithoutUpgradingButton] - Whether to hide the "Continue without upgrading" button.
|
* @property {boolean} [hideContinueWithoutUpgradingButton] - Whether to hide the "Continue without upgrading" button.
|
||||||
|
* @property {boolean} [redirectOnCompletion] - Whether to redirect after successful upgrade. Premium upgrades redirect to subscription settings, Families upgrades redirect to organization vault.
|
||||||
*/
|
*/
|
||||||
export type UnifiedUpgradeDialogParams = {
|
export type UnifiedUpgradeDialogParams = {
|
||||||
account: Account;
|
account: Account;
|
||||||
@@ -57,6 +59,7 @@ export type UnifiedUpgradeDialogParams = {
|
|||||||
selectedPlan?: PersonalSubscriptionPricingTierId | null;
|
selectedPlan?: PersonalSubscriptionPricingTierId | null;
|
||||||
planSelectionStepTitleOverride?: string | null;
|
planSelectionStepTitleOverride?: string | null;
|
||||||
hideContinueWithoutUpgradingButton?: boolean;
|
hideContinueWithoutUpgradingButton?: boolean;
|
||||||
|
redirectOnCompletion?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -86,6 +89,7 @@ export class UnifiedUpgradeDialogComponent implements OnInit {
|
|||||||
constructor(
|
constructor(
|
||||||
private dialogRef: DialogRef<UnifiedUpgradeDialogResult>,
|
private dialogRef: DialogRef<UnifiedUpgradeDialogResult>,
|
||||||
@Inject(DIALOG_DATA) private params: UnifiedUpgradeDialogParams,
|
@Inject(DIALOG_DATA) private params: UnifiedUpgradeDialogParams,
|
||||||
|
private router: Router,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
@@ -142,7 +146,20 @@ export class UnifiedUpgradeDialogComponent implements OnInit {
|
|||||||
default:
|
default:
|
||||||
status = UnifiedUpgradeDialogStatus.Closed;
|
status = UnifiedUpgradeDialogStatus.Closed;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.close({ status, organizationId: result.organizationId });
|
this.close({ status, organizationId: result.organizationId });
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.params.redirectOnCompletion &&
|
||||||
|
(status === UnifiedUpgradeDialogStatus.UpgradedToPremium ||
|
||||||
|
status === UnifiedUpgradeDialogStatus.UpgradedToFamilies)
|
||||||
|
) {
|
||||||
|
const redirectUrl =
|
||||||
|
status === UnifiedUpgradeDialogStatus.UpgradedToFamilies
|
||||||
|
? `/organizations/${result.organizationId}/vault`
|
||||||
|
: "/settings/subscription/user-subscription";
|
||||||
|
void this.router.navigate([redirectUrl]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -11851,5 +11851,14 @@
|
|||||||
},
|
},
|
||||||
"upgradeErrorMessage": {
|
"upgradeErrorMessage": {
|
||||||
"message": "We encountered an error while processing your upgrade. Please try again."
|
"message": "We encountered an error while processing your upgrade. Please try again."
|
||||||
|
},
|
||||||
|
"bitwardenFreeplanMessage": {
|
||||||
|
"message": "You have the Bitwarden Free plan"
|
||||||
|
},
|
||||||
|
"upgradeCompleteSecurity": {
|
||||||
|
"message": "Upgrade for complete security"
|
||||||
|
},
|
||||||
|
"viewbusinessplans": {
|
||||||
|
"message": "View business plans"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export enum FeatureFlag {
|
|||||||
PM24032_NewNavigationPremiumUpgradeButton = "pm-24032-new-navigation-premium-upgrade-button",
|
PM24032_NewNavigationPremiumUpgradeButton = "pm-24032-new-navigation-premium-upgrade-button",
|
||||||
PM25379_UseNewOrganizationMetadataStructure = "pm-25379-use-new-organization-metadata-structure",
|
PM25379_UseNewOrganizationMetadataStructure = "pm-25379-use-new-organization-metadata-structure",
|
||||||
PM24996_ImplementUpgradeFromFreeDialog = "pm-24996-implement-upgrade-from-free-dialog",
|
PM24996_ImplementUpgradeFromFreeDialog = "pm-24996-implement-upgrade-from-free-dialog",
|
||||||
|
PM24033PremiumUpgradeNewDesign = "pm-24033-updat-premium-subscription-page",
|
||||||
|
|
||||||
/* Key Management */
|
/* Key Management */
|
||||||
PrivateKeyRegeneration = "pm-12241-private-key-regeneration",
|
PrivateKeyRegeneration = "pm-12241-private-key-regeneration",
|
||||||
@@ -106,6 +107,7 @@ export const DefaultFeatureFlagValue = {
|
|||||||
[FeatureFlag.PM24032_NewNavigationPremiumUpgradeButton]: FALSE,
|
[FeatureFlag.PM24032_NewNavigationPremiumUpgradeButton]: FALSE,
|
||||||
[FeatureFlag.PM25379_UseNewOrganizationMetadataStructure]: FALSE,
|
[FeatureFlag.PM25379_UseNewOrganizationMetadataStructure]: FALSE,
|
||||||
[FeatureFlag.PM24996_ImplementUpgradeFromFreeDialog]: FALSE,
|
[FeatureFlag.PM24996_ImplementUpgradeFromFreeDialog]: FALSE,
|
||||||
|
[FeatureFlag.PM24033PremiumUpgradeNewDesign]: FALSE,
|
||||||
|
|
||||||
/* Key Management */
|
/* Key Management */
|
||||||
[FeatureFlag.PrivateKeyRegeneration]: FALSE,
|
[FeatureFlag.PrivateKeyRegeneration]: FALSE,
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Tagline with consistent height (exactly 2 lines) -->
|
<!-- Tagline with consistent height (exactly 2 lines) -->
|
||||||
<div class="tw-mb-6 tw-h-12">
|
<div class="tw-mb-6 tw-h-6">
|
||||||
<p bitTypography="helper" class="tw-text-muted tw-m-0 tw-leading-relaxed tw-line-clamp-2">
|
<p bitTypography="helper" class="tw-text-muted tw-m-0 tw-leading-relaxed tw-line-clamp-2">
|
||||||
{{ tagline() }}
|
{{ tagline() }}
|
||||||
</p>
|
</p>
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
<div class="tw-mb-6">
|
<div class="tw-mb-6">
|
||||||
<div class="tw-flex tw-items-baseline tw-gap-1 tw-flex-wrap">
|
<div class="tw-flex tw-items-baseline tw-gap-1 tw-flex-wrap">
|
||||||
<span class="tw-text-3xl tw-font-bold tw-leading-none tw-m-0">{{
|
<span class="tw-text-3xl tw-font-bold tw-leading-none tw-m-0">{{
|
||||||
priceValue.amount | currency: "USD" : "symbol"
|
priceValue.amount | currency: "$"
|
||||||
}}</span>
|
}}</span>
|
||||||
<span bitTypography="helper" class="tw-text-muted">
|
<span bitTypography="helper" class="tw-text-muted">
|
||||||
/ {{ priceValue.cadence }}
|
/ {{ priceValue.cadence }}
|
||||||
|
|||||||
Reference in New Issue
Block a user