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:
@@ -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
|
||||
× {{ 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>
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user