1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-24 00:23:17 +00:00

[PM-29604] [PM-29605] [PM-29606] Premium subscription page redesign (#18300)

* refactor(subscription-card): Correctly name card action

* feat(storage-card): Switch 'callsToActionDisabled' into 1 input per CTA

* refactor(additional-options-card): Switch 'callsToActionDisabled' into 1 input per CTA

* feat(contract-alignment): Remove 'active' property from Discount

* feat(contract-alignment): Rename 'name' to 'translationKey' in Cart model

* feat(response-models): Add DiscountResponse

* feat(response-models): Add StorageResponse

* feat(response-models): Add CartResponse

* feat(response-models): Add BitwardenSubscriptionResponse

* feat(clients): Add new endpoint invocations

* feat(redesign): Add feature flags

* feat(redesign): Add AdjustAccountSubscriptionStorageDialogComponent

* feat(redesign): Add AccountSubscriptionComponent

* feat(redesign): Pivot subscription component on FF

* docs: Note FF removal POIs

* fix(subscription-card): Resolve compilation error in stories

* fix(upgrade-payment.service): Fix failing tests
This commit is contained in:
Alex Morask
2026-01-12 10:45:12 -06:00
committed by jaasen-livefront
parent 0bac1e8c16
commit 77c1ec1251
44 changed files with 1238 additions and 271 deletions

View File

@@ -1,6 +1,8 @@
import { Injectable } from "@angular/core";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { BitwardenSubscriptionResponse } from "@bitwarden/common/billing/models/response/bitwarden-subscription.response";
import { BitwardenSubscription } from "@bitwarden/subscription";
import {
BillingAddress,
@@ -11,13 +13,22 @@ import {
@Injectable()
export class AccountBillingClient {
private endpoint = "/account/billing/vnext";
private apiService: ApiService;
constructor(apiService: ApiService) {
this.apiService = apiService;
}
constructor(private apiService: ApiService) {}
purchasePremiumSubscription = async (
getLicense = async (): Promise<string> => {
const path = `${this.endpoint}/license`;
return this.apiService.send("GET", path, null, true, true);
};
getSubscription = async (): Promise<BitwardenSubscription> => {
const path = `${this.endpoint}/subscription`;
const json = await this.apiService.send("GET", path, null, true, true);
const response = new BitwardenSubscriptionResponse(json);
return response.toDomain();
};
purchaseSubscription = async (
paymentMethod: TokenizedPaymentMethod | NonTokenizedPaymentMethod,
billingAddress: Pick<BillingAddress, "country" | "postalCode">,
): Promise<void> => {
@@ -29,6 +40,17 @@ export class AccountBillingClient {
const request = isTokenizedPayment
? { tokenizedPaymentMethod: paymentMethod, billingAddress: billingAddress }
: { nonTokenizedPaymentMethod: paymentMethod, billingAddress: billingAddress };
await this.apiService.send("POST", path, request, true, true);
};
reinstateSubscription = async (): Promise<void> => {
const path = `${this.endpoint}/subscription/reinstate`;
await this.apiService.send("POST", path, null, true, false);
};
updateSubscriptionStorage = async (additionalStorageGb: number): Promise<void> => {
const path = `${this.endpoint}/subscription/storage`;
await this.apiService.send("PUT", path, { additionalStorageGb }, true, false);
};
}

View File

@@ -1,9 +1,12 @@
import { inject, 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 { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { AccountPaymentDetailsComponent } from "@bitwarden/web-vault/app/billing/individual/payment-details/account-payment-details.component";
import { SelfHostedPremiumComponent } from "@bitwarden/web-vault/app/billing/individual/premium/self-hosted-premium.component";
import { AccountSubscriptionComponent } from "@bitwarden/web-vault/app/billing/individual/subscription/account-subscription.component";
import { BillingHistoryViewComponent } from "./billing-history-view.component";
import { CloudHostedPremiumComponent } from "./premium/cloud-hosted-premium.component";
@@ -17,11 +20,15 @@ const routes: Routes = [
data: { titleId: "subscription" },
children: [
{ path: "", pathMatch: "full", redirectTo: "premium" },
{
path: "user-subscription",
component: UserSubscriptionComponent,
data: { titleId: "premiumMembership" },
},
...featureFlaggedRoute({
defaultComponent: UserSubscriptionComponent,
flaggedComponent: AccountSubscriptionComponent,
featureFlag: FeatureFlag.PM29594_UpdateIndividualSubscriptionPage,
routeOptions: {
path: "user-subscription",
data: { titleId: "premiumMembership" },
},
}),
/**
* Two-Route Matching Strategy for /premium:
*

View File

@@ -0,0 +1,50 @@
@if (subscriptionLoading()) {
<ng-container>
<i
class="bwi bwi-spinner bwi-spin tw-text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
</ng-container>
} @else {
@if (subscription.value(); as subscription) {
<!-- Page Header -->
<div
class="tw-flex tw-flex-col tw-gap-3 tw-items-center tw-text-center tw-pb-8 tw-pt-12 tw-px-14"
>
<h1 bitTypography="h1" class="tw-m-0">{{ "youHavePremium" | i18n }}</h1>
<p bitTypography="body1" class="tw-m-0 tw-text-muted">
{{ "viewAndManagePremiumSubscription" | i18n }}
</p>
</div>
<!-- Content Container -->
<div class="tw-flex tw-flex-col tw-gap-10 tw-mx-auto tw-max-w-[800px] tw-pb-8">
<!-- Premium Membership Card -->
<billing-subscription-card
[title]="'premiumMembership' | i18n"
[subscription]="subscription"
[showUpgradeButton]="premiumToOrganizationUpgradeEnabled()"
(callToActionClicked)="onSubscriptionCardAction($event)"
/>
<!-- Storage Card -->
@if (subscription.storage; as storage) {
<billing-storage-card
[storage]="storage"
[addStorageDisabled]="!canAddStorage()"
[removeStorageDisabled]="!canRemoveStorage()"
(callToActionClicked)="onStorageCardAction($event)"
/>
}
<!-- Additional Options Card -->
<billing-additional-options-card
[downloadLicenseDisabled]="subscriptionTerminal()"
[cancelSubscriptionDisabled]="!canCancelSubscription()"
(callToActionClicked)="onAdditionalOptionsCardAction($event)"
/>
</div>
}
}

View File

@@ -0,0 +1,291 @@
import { ChangeDetectionStrategy, Component, computed, inject, resource } from "@angular/core";
import { toSignal } from "@angular/core/rxjs-interop";
import { ActivatedRoute, Router } from "@angular/router";
import { firstValueFrom, lastValueFrom, map } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
import { SubscriptionPricingServiceAbstraction } from "@bitwarden/common/billing/abstractions/subscription-pricing.service.abstraction";
import { PersonalSubscriptionPricingTierIds } from "@bitwarden/common/billing/types/subscription-pricing-tier";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { DialogService, ToastService, TypographyModule } from "@bitwarden/components";
import { Maybe } from "@bitwarden/pricing";
import {
AdditionalOptionsCardAction,
AdditionalOptionsCardActions,
AdditionalOptionsCardComponent,
MAX_STORAGE_GB,
Storage,
StorageCardAction,
StorageCardActions,
StorageCardComponent,
SubscriptionCardAction,
SubscriptionCardActions,
SubscriptionCardComponent,
SubscriptionStatuses,
} from "@bitwarden/subscription";
import { I18nPipe } from "@bitwarden/ui-common";
import { AccountBillingClient } from "@bitwarden/web-vault/app/billing/clients";
import {
AdjustAccountSubscriptionStorageDialogComponent,
AdjustAccountSubscriptionStorageDialogParams,
} from "@bitwarden/web-vault/app/billing/individual/subscription/adjust-account-subscription-storage-dialog.component";
import {
OffboardingSurveyDialogResultType,
openOffboardingSurvey,
} from "@bitwarden/web-vault/app/billing/shared/offboarding-survey.component";
@Component({
templateUrl: "./account-subscription.component.html",
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
AdditionalOptionsCardComponent,
I18nPipe,
JslibModule,
StorageCardComponent,
SubscriptionCardComponent,
TypographyModule,
],
providers: [AccountBillingClient],
})
export class AccountSubscriptionComponent {
private accountService = inject(AccountService);
private activatedRoute = inject(ActivatedRoute);
private accountBillingClient = inject(AccountBillingClient);
private billingAccountProfileStateService = inject(BillingAccountProfileStateService);
private configService = inject(ConfigService);
private dialogService = inject(DialogService);
private fileDownloadService = inject(FileDownloadService);
private i18nService = inject(I18nService);
private router = inject(Router);
private subscriptionPricingService = inject(SubscriptionPricingServiceAbstraction);
private toastService = inject(ToastService);
readonly subscription = resource({
loader: async () => {
const redirectToPremiumPage = async (): Promise<null> => {
await this.router.navigate(["/settings/subscription/premium"]);
return null;
};
const account = await firstValueFrom(this.accountService.activeAccount$);
if (!account) {
return await redirectToPremiumPage();
}
const hasPremiumPersonally = await firstValueFrom(
this.billingAccountProfileStateService.hasPremiumPersonally$(account.id),
);
if (!hasPremiumPersonally) {
return await redirectToPremiumPage();
}
return await this.accountBillingClient.getSubscription();
},
});
readonly subscriptionLoading = computed<boolean>(() => this.subscription.isLoading());
readonly subscriptionTerminal = computed<Maybe<boolean>>(() => {
const subscription = this.subscription.value();
if (subscription) {
return (
subscription.status === SubscriptionStatuses.IncompleteExpired ||
subscription.status === SubscriptionStatuses.Canceled ||
subscription.status === SubscriptionStatuses.Unpaid
);
}
});
readonly subscriptionPendingCancellation = computed<Maybe<boolean>>(() => {
const subscription = this.subscription.value();
if (subscription) {
return (
(subscription.status === SubscriptionStatuses.Trialing ||
subscription.status === SubscriptionStatuses.Active) &&
!!subscription.cancelAt
);
}
});
readonly storage = computed<Maybe<Storage>>(() => {
const subscription = this.subscription.value();
return subscription?.storage;
});
readonly purchasedStorage = computed<number | undefined>(() => {
const subscription = this.subscription.value();
return subscription?.cart.passwordManager.additionalStorage?.quantity;
});
readonly premiumPlan = toSignal(
this.subscriptionPricingService
.getPersonalSubscriptionPricingTiers$()
.pipe(
map((tiers) =>
tiers.find((tier) => tier.id === PersonalSubscriptionPricingTierIds.Premium),
),
),
);
readonly premiumStoragePrice = computed<Maybe<number>>(() => {
const premiumPlan = this.premiumPlan();
return premiumPlan?.passwordManager.annualPricePerAdditionalStorageGB;
});
readonly premiumProvidedStorage = computed<Maybe<number>>(() => {
const premiumPlan = this.premiumPlan();
return premiumPlan?.passwordManager.providedStorageGB;
});
readonly canAddStorage = computed<Maybe<boolean>>(() => {
if (this.subscriptionTerminal()) {
return false;
}
const storage = this.storage();
const premiumProvidedStorage = this.premiumProvidedStorage();
if (storage && premiumProvidedStorage) {
const maxAttainableStorage = MAX_STORAGE_GB - premiumProvidedStorage;
return storage.available < maxAttainableStorage;
}
});
readonly canRemoveStorage = computed<Maybe<boolean>>(() => {
if (this.subscriptionTerminal()) {
return false;
}
const purchasedStorage = this.purchasedStorage();
if (!purchasedStorage || purchasedStorage === 0) {
return false;
}
const storage = this.storage();
if (storage) {
return storage.available > storage.used;
}
});
readonly canCancelSubscription = computed<Maybe<boolean>>(() => {
if (this.subscriptionTerminal()) {
return false;
}
return !this.subscriptionPendingCancellation();
});
readonly premiumToOrganizationUpgradeEnabled = toSignal(
this.configService.getFeatureFlag$(FeatureFlag.PM29593_PremiumToOrganizationUpgrade),
{ initialValue: false },
);
onSubscriptionCardAction = async (action: SubscriptionCardAction) => {
switch (action) {
case SubscriptionCardActions.ContactSupport:
window.open("https://bitwarden.com/contact/", "_blank");
break;
case SubscriptionCardActions.ManageInvoices:
await this.router.navigate(["../billing-history"], { relativeTo: this.activatedRoute });
break;
case SubscriptionCardActions.ReinstateSubscription: {
const confirmed = await this.dialogService.openSimpleDialog({
title: { key: "reinstateSubscription" },
content: { key: "reinstateConfirmation" },
type: "warning",
});
if (!confirmed) {
return;
}
await this.accountBillingClient.reinstateSubscription();
this.toastService.showToast({
variant: "success",
title: "",
message: this.i18nService.t("reinstated"),
});
this.subscription.reload();
break;
}
case SubscriptionCardActions.UpdatePayment:
await this.router.navigate(["../payment-details"], { relativeTo: this.activatedRoute });
break;
case SubscriptionCardActions.UpgradePlan:
// TODO: Implement upgrade plan navigation
break;
}
};
onStorageCardAction = async (action: StorageCardAction) => {
const data = this.getAdjustStorageDialogParams(action);
const dialogReference = AdjustAccountSubscriptionStorageDialogComponent.open(
this.dialogService,
{
data,
},
);
const result = await lastValueFrom(dialogReference.closed);
if (result === "submitted") {
this.subscription.reload();
}
};
onAdditionalOptionsCardAction = async (action: AdditionalOptionsCardAction) => {
switch (action) {
case AdditionalOptionsCardActions.DownloadLicense: {
const license = await this.accountBillingClient.getLicense();
const json = JSON.stringify(license, null, 2);
this.fileDownloadService.download({
fileName: "bitwarden_premium_license.json",
blobData: json,
});
break;
}
case AdditionalOptionsCardActions.CancelSubscription: {
const dialogReference = openOffboardingSurvey(this.dialogService, {
data: {
type: "User",
},
});
const result = await lastValueFrom(dialogReference.closed);
if (result === OffboardingSurveyDialogResultType.Closed) {
return;
}
this.subscription.reload();
}
}
};
getAdjustStorageDialogParams = (
action: StorageCardAction,
): Maybe<AdjustAccountSubscriptionStorageDialogParams> => {
const purchasedStorage = this.purchasedStorage();
const storagePrice = this.premiumStoragePrice();
const providedStorage = this.premiumProvidedStorage();
switch (action) {
case StorageCardActions.AddStorage: {
if (storagePrice && providedStorage) {
return {
type: "add",
price: storagePrice,
provided: providedStorage,
cadence: "annually",
existing: purchasedStorage,
};
}
break;
}
case StorageCardActions.RemoveStorage: {
if (purchasedStorage) {
return {
type: "remove",
existing: purchasedStorage,
};
}
break;
}
}
};
}

View File

@@ -0,0 +1,43 @@
@let content = this.content();
<form [formGroup]="formGroup" [bitSubmit]="submit">
<bit-dialog [title]="content.title">
<ng-container bitDialogContent>
<p bitTypography="body1">{{ content.body }}</p>
<div class="tw-grid tw-grid-cols-12">
<bit-form-field class="tw-col-span-7">
<bit-label>{{ content.label }}</bit-label>
<input bitInput type="number" [formControl]="formGroup.controls.amount" />
@if (action() === "add") {
<bit-hint>
<!-- Total: 10 GB × $0.50 = $5.00 /month -->
<strong>{{ "total" | i18n }}</strong>
{{ formGroup.value.amount }} GB &times; {{ price() | currency: "$" }} =
{{ price() * formGroup.value.amount | currency: "$" }} /
{{ term() | i18n }}
</bit-hint>
}
</bit-form-field>
</div>
</ng-container>
<ng-container bitDialogFooter>
<button
type="submit"
bitButton
bitFormButton
buttonType="primary"
[disabled]="formGroup.invalid"
>
{{ "submit" | i18n }}
</button>
<button
type="button"
bitButton
bitFormButton
buttonType="secondary"
[bitDialogClose]="'closed'"
>
{{ "cancel" | i18n }}
</button>
</ng-container>
</bit-dialog>
</form>

View File

@@ -0,0 +1,182 @@
import { CurrencyPipe } from "@angular/common";
import { ChangeDetectionStrategy, Component, computed, inject } from "@angular/core";
import { FormControl, FormGroup, ReactiveFormsModule, Validators } from "@angular/forms";
import { SubscriptionCadence } from "@bitwarden/common/billing/types/subscription-pricing-tier";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import {
AsyncActionsModule,
ButtonModule,
DIALOG_DATA,
DialogConfig,
DialogModule,
DialogRef,
DialogService,
FormFieldModule,
ToastService,
TypographyModule,
} from "@bitwarden/components";
import { Maybe } from "@bitwarden/pricing";
import { MAX_STORAGE_GB } from "@bitwarden/subscription";
import { I18nPipe } from "@bitwarden/ui-common";
import { AccountBillingClient } from "@bitwarden/web-vault/app/billing/clients";
type RemoveStorage = {
type: "remove";
existing: number;
};
type AddStorage = {
type: "add";
price: number;
provided: number;
cadence: SubscriptionCadence;
existing?: number;
};
export type AdjustAccountSubscriptionStorageDialogParams = RemoveStorage | AddStorage;
type AdjustAccountSubscriptionStorageDialogResult = "closed" | "submitted";
@Component({
templateUrl: "./adjust-account-subscription-storage-dialog.component.html",
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [AccountBillingClient],
imports: [
AsyncActionsModule,
ButtonModule,
CurrencyPipe,
DialogModule,
FormFieldModule,
I18nPipe,
ReactiveFormsModule,
TypographyModule,
],
})
export class AdjustAccountSubscriptionStorageDialogComponent {
private readonly accountBillingClient = inject(AccountBillingClient);
private readonly dialogParams = inject<AdjustAccountSubscriptionStorageDialogParams>(DIALOG_DATA);
private readonly dialogRef = inject(DialogRef<AdjustAccountSubscriptionStorageDialogResult>);
private readonly i18nService = inject(I18nService);
private readonly toastService = inject(ToastService);
readonly action = computed<"add" | "remove">(() => this.dialogParams.type);
readonly price = computed<Maybe<number>>(() => {
if (this.dialogParams.type === "add") {
return this.dialogParams.price;
}
});
readonly provided = computed<Maybe<number>>(() => {
if (this.dialogParams.type === "add") {
return this.dialogParams.provided;
}
});
readonly term = computed<Maybe<string>>(() => {
if (this.dialogParams.type === "add") {
switch (this.dialogParams.cadence) {
case "annually":
return this.i18nService.t("year");
case "monthly":
return this.i18nService.t("month");
}
}
});
readonly existing = computed<Maybe<number>>(() => this.dialogParams.existing);
readonly content = computed<{
title: string;
body: string;
label: string;
}>(() => {
const action = this.action();
switch (action) {
case "add":
return {
title: this.i18nService.t("addStorage"),
body: this.i18nService.t("storageAddNote"),
label: this.i18nService.t("gbStorageAdd"),
};
case "remove":
return {
title: this.i18nService.t("removeStorage"),
body: this.i18nService.t("whenYouRemoveStorage"),
label: this.i18nService.t("gbStorageRemove"),
};
}
});
readonly maxPurchasable = computed<Maybe<number>>(() => {
const provided = this.provided();
if (provided) {
return MAX_STORAGE_GB - provided;
}
});
readonly maxValidatorValue = computed<number>(() => {
const maxPurchasable = this.maxPurchasable() ?? MAX_STORAGE_GB;
const existing = this.existing();
const action = this.action();
switch (action) {
case "add": {
return existing ? maxPurchasable - existing : maxPurchasable;
}
case "remove": {
return existing ? existing : 0;
}
}
});
formGroup = new FormGroup({
amount: new FormControl<number>(1, {
nonNullable: true,
validators: [
Validators.required,
Validators.min(1),
Validators.max(this.maxValidatorValue()),
],
}),
});
submit = async () => {
this.formGroup.markAllAsTouched();
if (!this.formGroup.valid || !this.formGroup.value.amount) {
return;
}
const action = this.action();
const existing = this.existing();
const amount = this.formGroup.value.amount;
switch (action) {
case "add": {
await this.accountBillingClient.updateSubscriptionStorage(amount + (existing ?? 0));
break;
}
case "remove": {
await this.accountBillingClient.updateSubscriptionStorage(existing! - amount);
}
}
this.toastService.showToast({
variant: "success",
title: "",
message: this.i18nService.t("adjustedStorage", amount),
});
this.dialogRef.close("submitted");
};
static open = (
dialogService: DialogService,
dialogConfig: DialogConfig<AdjustAccountSubscriptionStorageDialogParams>,
) =>
dialogService.open<AdjustAccountSubscriptionStorageDialogResult>(
AdjustAccountSubscriptionStorageDialogComponent,
dialogConfig,
);
}

View File

@@ -478,13 +478,13 @@ describe("UpgradePaymentService", () => {
describe("upgradeToPremium", () => {
it("should call accountBillingClient to purchase premium subscription and refresh data", async () => {
// Arrange
mockAccountBillingClient.purchasePremiumSubscription.mockResolvedValue();
mockAccountBillingClient.purchaseSubscription.mockResolvedValue();
// Act
await sut.upgradeToPremium(mockTokenizedPaymentMethod, mockBillingAddress);
// Assert
expect(mockAccountBillingClient.purchasePremiumSubscription).toHaveBeenCalledWith(
expect(mockAccountBillingClient.purchaseSubscription).toHaveBeenCalledWith(
mockTokenizedPaymentMethod,
mockBillingAddress,
);
@@ -496,13 +496,13 @@ describe("UpgradePaymentService", () => {
const accountCreditPaymentMethod: NonTokenizedPaymentMethod = {
type: NonTokenizablePaymentMethods.accountCredit,
};
mockAccountBillingClient.purchasePremiumSubscription.mockResolvedValue();
mockAccountBillingClient.purchaseSubscription.mockResolvedValue();
// Act
await sut.upgradeToPremium(accountCreditPaymentMethod, mockBillingAddress);
// Assert
expect(mockAccountBillingClient.purchasePremiumSubscription).toHaveBeenCalledWith(
expect(mockAccountBillingClient.purchaseSubscription).toHaveBeenCalledWith(
accountCreditPaymentMethod,
mockBillingAddress,
);

View File

@@ -143,7 +143,7 @@ export class UpgradePaymentService {
): Promise<void> {
this.validatePaymentAndBillingInfo(paymentMethod, billingAddress);
await this.accountBillingClient.purchasePremiumSubscription(paymentMethod, billingAddress);
await this.accountBillingClient.purchaseSubscription(paymentMethod, billingAddress);
await this.refreshAndSync();
}

View File

@@ -142,7 +142,7 @@ export class UpgradePaymentComponent implements OnInit, AfterViewInit {
if (!this.selectedPlan()) {
return {
passwordManager: {
seats: { name: "", cost: 0, quantity: 0 },
seats: { translationKey: "", cost: 0, quantity: 0 },
},
cadence: "annually",
estimatedTax: 0,
@@ -152,7 +152,7 @@ export class UpgradePaymentComponent implements OnInit, AfterViewInit {
return {
passwordManager: {
seats: {
name: this.isFamiliesPlan ? "familiesMembership" : "premiumMembership",
translationKey: this.isFamiliesPlan ? "familiesMembership" : "premiumMembership",
cost: this.selectedPlan()!.details.passwordManager.annualPrice ?? 0,
quantity: 1,
},

View File

@@ -30,6 +30,7 @@ import {
import { UpdateLicenseDialogComponent } from "../shared/update-license-dialog.component";
import { UpdateLicenseDialogResult } from "../shared/update-license-types";
// TODO: Remove with deletion of pm-29594-update-individual-subscription-page
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
@Component({
@@ -256,8 +257,8 @@ export class UserSubscriptionComponent implements OnInit {
return null;
}
return discount.amountOff
? { type: DiscountTypes.AmountOff, active: discount.active, value: discount.amountOff }
: { type: DiscountTypes.PercentOff, active: discount.active, value: discount.percentOff };
? { type: DiscountTypes.AmountOff, value: discount.amountOff }
: { type: DiscountTypes.PercentOff, value: discount.percentOff };
}
get isSubscriptionActive(): boolean {

View File

@@ -12614,5 +12614,11 @@
},
"storageFullDescription": {
"message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage."
},
"whenYouRemoveStorage": {
"message": "When you remove storage, you will receive a prorated account credit that will automatically go toward your next bill."
},
"youHavePremium": {
"message": "You have Premium"
}
}
}