1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-09 05:00:10 +00:00

Add the feature flag

This commit is contained in:
Cy Okeke
2025-09-29 15:42:12 +01:00
parent 46342498f4
commit 8095ac3ed2
4 changed files with 166 additions and 52 deletions

View File

@@ -5,13 +5,15 @@
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">
You have the Bitwarden Free plan
{{ "bitwardenFreeplanMessage" | i18n }}
</span>
</div>
<h2 *ngIf="!isSelfHost" class="tw-mt-6 tw-text-4xl">Upgrade for complete security</h2>
<h2 *ngIf="!isSelfHost" class="tw-mt-6 tw-text-4xl">
{{ "upgradeCompleteSecurity" | i18n }}
</h2>
<p class="tw-text-muted tw-mb-6">
Unlock more security features with Premium, or start sharing items with Families
{{ "unlockPremiumFeatures" | i18n }}
</p>
</div>
@@ -21,13 +23,13 @@
<div>
@if (premiumCardData$ | async; as premiumData) {
<billing-pricing-card
[tagline]="'Complete online security'"
[tagline]="'planDescPremium' | i18n"
[price]="{ amount: premiumData.price, cadence: 'monthly' }"
[button]="{ type: 'primary', text: 'Upgrade to Premium' }"
[button]="{ type: 'primary', text: ('upgradeToPremium' | i18n) }"
[features]="premiumData.features"
(buttonClick)="openUpgradeDialog('Premium')"
>
<h3 slot="title" bitTypography="h3" class="tw-m-0">Premium</h3>
<h3 slot="title" bitTypography="h3" class="tw-m-0">{{ "premium" | i18n }}</h3>
</billing-pricing-card>
}
</div>
@@ -36,13 +38,13 @@
<div>
@if (familiesCardData$ | async; as familiesData) {
<billing-pricing-card
[tagline]="'Premium security for your family'"
[tagline]="'planDescFamiliesV2' | i18n"
[price]="{ amount: familiesData.price, cadence: 'monthly' }"
[button]="{ type: 'secondary', text: 'Upgrade to Families' }"
[button]="{ type: 'secondary', text: ('upgradeToFamilies' | i18n) }"
[features]="familiesData.features"
(buttonClick)="openUpgradeDialog('Families')"
>
<h3 slot="title" bitTypography="h3" class="tw-m-0">Families</h3>
<h3 slot="title" bitTypography="h3" class="tw-m-0">{{ "families" | i18n }}</h3>
</billing-pricing-card>
}
</div>
@@ -50,7 +52,7 @@
<!-- Business Plans Link -->
<div class="tw-text-center tw-mt-6">
<p class="tw-text-muted tw-mb-2">Prices exclude tax and are billed annually</p>
<p class="tw-text-muted tw-mb-2">{{ "individualUpgradeTaxInformationMessage" | i18n }}</p>
<a
bitLink
linkType="primary"
@@ -58,13 +60,15 @@
target="_blank"
rel="noopener noreferrer"
>
View business plans <i class="bwi bwi-external-link tw-ml-1" aria-hidden="true"></i>
{{ "viewbusinessplans" | i18n }}
<i class="bwi bwi-external-link tw-ml-1" aria-hidden="true"></i>
</a>
</div>
</bit-section>
<!-- Legacy Design (shown when user already has premium access) -->
<bit-section *ngIf="!(shouldShowNewDesign$ | async)">
</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"
@@ -109,9 +113,7 @@
<p bitTypography="body1" [ngClass]="{ 'tw-mb-0': !isSelfHost }">
{{
"premiumPriceWithFamilyPlan"
| i18n
: (((getPremiumPrice() | async) || 0) * 12 | currency: "$")
: familyPlanMaxUserCount
| i18n: (premiumPrice | currency: "$") : familyPlanMaxUserCount
}}
<a
bitLink
@@ -139,4 +141,51 @@
(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>
<bit-section>
<h2 bitTypography="h2">{{ "summary" | i18n }}</h2>
{{ "premiumMembership" | i18n }}: {{ premiumPrice | currency: "$" }} <br />
{{ "additionalStorageGb" | i18n }}: {{ addOnFormGroup.value.additionalStorage || 0 }} GB
&times; {{ 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>

View File

@@ -1,17 +1,19 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component } from "@angular/core";
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 { map, shareReplay } from "rxjs/operators";
import { debounceTime, map, shareReplay } from "rxjs/operators";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
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";
@@ -19,12 +21,14 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
import { SyncService } from "@bitwarden/common/platform/sync";
import { DialogService, ToastService } from "@bitwarden/components";
import { SubscriptionPricingService } from "../../services/subscription-pricing.service";
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,
@@ -35,6 +39,9 @@ import {
standalone: false,
})
export class PremiumComponent {
@ViewChild(PaymentComponent) paymentComponent: PaymentComponent;
@ViewChild(TaxInfoComponent) taxInfoComponent: TaxInfoComponent;
protected hasPremiumFromAnyOrganization$: Observable<boolean>;
protected hasPremiumPersonally$: Observable<boolean>;
protected shouldShowNewDesign$: Observable<boolean>;
@@ -60,11 +67,12 @@ export class PremiumComponent {
protected cloudWebVaultURL: string;
protected isSelfHost = false;
protected providerId: string;
protected subscriber: BitwardenSubscriber;
protected estimatedTax: number = 0;
protected readonly familyPlanMaxUserCount = 6;
protected readonly premiumPrice = 10;
protected readonly storageGBPrice = 4;
constructor(
private activatedRoute: ActivatedRoute,
@@ -106,7 +114,13 @@ export class PremiumComponent {
this.shouldShowNewDesign$ = combineLatest([
this.hasPremiumFromAnyOrganization$,
this.hasPremiumPersonally$,
]).pipe(map(([hasOrgPremium, hasPersonalPremium]) => !hasOrgPremium && !hasPersonalPremium));
this.configService.getFeatureFlag$(FeatureFlag.PremiumUpgradeNewDesign),
]).pipe(
map(
([hasOrgPremium, hasPersonalPremium, isNewDesignEnabled]) =>
isNewDesignEnabled && !hasOrgPremium && !hasPersonalPremium,
),
);
this.personalPricingTiers$ =
this.subscriptionPricingService.getPersonalSubscriptionPricingTiers$();
@@ -162,10 +176,10 @@ export class PremiumComponent {
)
.subscribe();
this.activatedRoute.parent.parent.parent.params
.pipe(takeUntilDestroyed())
.subscribe((params) => {
this.providerId = params.providerId;
this.addOnFormGroup.controls.additionalStorage.valueChanges
.pipe(debounceTime(1000), takeUntilDestroyed())
.subscribe(() => {
this.refreshSalesTax();
});
}
@@ -219,14 +233,78 @@ export class PremiumComponent {
await this.postFinalizeUpgrade();
};
submitPayment = async (): Promise<void> => {
this.taxInfoComponent.taxFormGroup.markAllAsTouched();
if (this.taxInfoComponent.taxFormGroup.invalid) {
return;
}
const { type, token } = await this.paymentComponent.tokenize();
const formData = new FormData();
formData.append("paymentMethodType", type.toString());
formData.append("paymentToken", token);
formData.append("additionalStorageGb", this.addOnFormGroup.value.additionalStorage.toString());
formData.append("country", this.taxInfoComponent.country);
formData.append("postalCode", this.taxInfoComponent.postalCode);
await this.apiService.postPremium(formData);
await this.finalizeUpgrade();
await this.postFinalizeUpgrade();
};
protected get additionalStorageCost(): number {
return this.storageGBPrice * this.addOnFormGroup.value.additionalStorage;
}
protected get premiumURL(): string {
return `${this.cloudWebVaultURL}/#/settings/subscription/premium`;
}
protected get subtotal(): number {
return this.premiumPrice + this.additionalStorageCost;
}
protected get total(): number {
return this.subtotal + this.estimatedTax;
}
protected async onLicenseFileSelectedChanged(): Promise<void> {
await this.postFinalizeUpgrade();
}
private refreshSalesTax(): void {
if (!this.taxInfoComponent.country || !this.taxInfoComponent.postalCode) {
return;
}
const request: PreviewIndividualInvoiceRequest = {
passwordManager: {
additionalStorage: this.addOnFormGroup.value.additionalStorage,
},
taxInformation: {
postalCode: this.taxInfoComponent.postalCode,
country: this.taxInfoComponent.country,
},
};
this.taxService
.previewIndividualInvoice(request)
.then((invoice) => {
this.estimatedTax = invoice.taxAmount;
})
.catch((error) => {
this.toastService.showToast({
title: "",
variant: "error",
message: this.i18nService.t(error.message),
});
});
}
protected onTaxInformationChanged(): void {
this.refreshSalesTax();
}
protected async openUpgradeDialog(type: "Premium" | "Families"): Promise<void> {
try {
const planId =
@@ -283,29 +361,4 @@ export class PremiumComponent {
break;
}
}
// Helper methods for backward compatibility (if needed elsewhere)
protected getPremiumTier(): Observable<PersonalSubscriptionPricingTier | undefined> {
return this.premiumCardData$.pipe(map((data) => data.tier));
}
protected getFamiliesTier(): Observable<PersonalSubscriptionPricingTier | undefined> {
return this.familiesCardData$.pipe(map((data) => data.tier));
}
protected getPremiumPrice(): Observable<number> {
return this.premiumCardData$.pipe(map((data) => data.price));
}
protected getFamiliesPrice(): Observable<number> {
return this.familiesCardData$.pipe(map((data) => data.price));
}
protected getPremiumFeatures(): Observable<string[]> {
return this.premiumCardData$.pipe(map((data) => data.features));
}
protected getFamiliesFeatures(): Observable<string[]> {
return this.familiesCardData$.pipe(map((data) => data.features));
}
}

View File

@@ -11521,5 +11521,17 @@
},
"organizationNameDescription": {
"message": "Your organization name will appear in invitations you send to members."
},
"bitwardenFreeplanMessage": {
"message": "You have the Bitwarden Free plan"
},
"upgradeCompleteSecurity": {
"message": "Upgrade for complete security"
},
"unlockPremiumFeatures": {
"message": "Unlock more security features with Premium, or start sharing items with Families"
},
"viewbusinessplans": {
"message": "View business plans"
}
}

View File

@@ -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 = "premium-upgrade-new-design",
PremiumUpgradeNewDesign = "pm-24033-updat-premium-subscription-page",
/* Key Management */
PrivateKeyRegeneration = "pm-12241-private-key-regeneration",