mirror of
https://github.com/bitwarden/browser
synced 2025-12-17 16:53:34 +00:00
[PM-29138] fix defect with pricing service on self host (#17819)
* [PM-29138] fix defect with pricing service on self host * use iscloud instead of manually checking region * fixing strict compile issues * spacing updates from design review * final spacing edits * pr feedback * typechecking
This commit is contained in:
@@ -20,33 +20,35 @@
|
||||
<div
|
||||
class="tw-box-border tw-bg-background tw-text-main tw-size-full tw-flex tw-flex-col tw-px-8 tw-pb-2 tw-w-full tw-max-w-md"
|
||||
>
|
||||
<div class="tw-flex tw-items-center tw-justify-between tw-mb-2">
|
||||
<div class="tw-flex tw-items-center tw-justify-between">
|
||||
<h3 slot="title" class="tw-m-0" bitTypography="h3">
|
||||
{{ "upgradeToPremium" | i18n }}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<!-- Tagline with consistent height (exactly 2 lines) -->
|
||||
<div class="tw-mb-6 tw-h-6">
|
||||
<div class="tw-h-6">
|
||||
<p bitTypography="helper" class="tw-text-muted tw-m-0 tw-leading-relaxed tw-line-clamp-2">
|
||||
{{ cardDetails.tagline }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Price Section -->
|
||||
<div class="tw-mb-6">
|
||||
<div class="tw-flex tw-items-baseline tw-gap-1 tw-flex-wrap">
|
||||
<span class="tw-text-3xl tw-font-medium tw-leading-none tw-m-0">{{
|
||||
cardDetails.price.amount | currency: "$"
|
||||
}}</span>
|
||||
<span bitTypography="helper" class="tw-text-muted">
|
||||
/ {{ cardDetails.price.cadence | i18n }}
|
||||
</span>
|
||||
@if (cardDetails.price) {
|
||||
<div class="tw-mt-5">
|
||||
<div class="tw-flex tw-items-baseline tw-gap-1 tw-flex-wrap">
|
||||
<span class="tw-text-3xl tw-font-medium tw-leading-none tw-m-0">{{
|
||||
cardDetails.price.amount | currency: "$"
|
||||
}}</span>
|
||||
<span bitTypography="helper" class="tw-text-muted">
|
||||
/ {{ cardDetails.price.cadence | i18n }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Button space (always reserved) -->
|
||||
<div class="tw-mb-6 tw-h-12">
|
||||
<div class="tw-my-5 tw-h-12">
|
||||
<button
|
||||
bitButton
|
||||
[buttonType]="cardDetails.button.type"
|
||||
|
||||
@@ -206,4 +206,39 @@ describe("PremiumUpgradeDialogComponent", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("self-hosted environment", () => {
|
||||
it("should handle null price data for self-hosted environment", async () => {
|
||||
const selfHostedPremiumTier: PersonalSubscriptionPricingTier = {
|
||||
id: PersonalSubscriptionPricingTierIds.Premium,
|
||||
name: "Premium",
|
||||
description: "Advanced features for power users",
|
||||
availableCadences: [SubscriptionCadenceIds.Annually],
|
||||
passwordManager: {
|
||||
type: "standalone",
|
||||
annualPrice: undefined as any, // self-host will have these prices empty
|
||||
annualPricePerAdditionalStorageGB: undefined as any,
|
||||
providedStorageGB: undefined as any,
|
||||
features: [
|
||||
{ key: "feature1", value: "Feature 1" },
|
||||
{ key: "feature2", value: "Feature 2" },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
mockSubscriptionPricingService.getPersonalSubscriptionPricingTiers$.mockReturnValue(
|
||||
of([selfHostedPremiumTier]),
|
||||
);
|
||||
|
||||
const selfHostedFixture = TestBed.createComponent(PremiumUpgradeDialogComponent);
|
||||
const selfHostedComponent = selfHostedFixture.componentInstance;
|
||||
selfHostedFixture.detectChanges();
|
||||
|
||||
const cardDetails = await firstValueFrom(selfHostedComponent["cardDetails$"]);
|
||||
|
||||
expect(cardDetails?.title).toBe("Premium");
|
||||
expect(cardDetails?.price).toBeUndefined();
|
||||
expect(cardDetails?.features).toEqual(["Feature 1", "Feature 2"]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -42,6 +42,23 @@ const mockPremiumTier: PersonalSubscriptionPricingTier = {
|
||||
},
|
||||
};
|
||||
|
||||
const mockPremiumTierNoPricingData: PersonalSubscriptionPricingTier = {
|
||||
id: PersonalSubscriptionPricingTierIds.Premium,
|
||||
name: "Premium",
|
||||
description: "Complete online security",
|
||||
availableCadences: [SubscriptionCadenceIds.Annually],
|
||||
passwordManager: {
|
||||
type: "standalone",
|
||||
features: [
|
||||
{ key: "builtInAuthenticator", value: "Built-in authenticator" },
|
||||
{ key: "secureFileStorage", value: "Secure file storage" },
|
||||
{ key: "emergencyAccess", value: "Emergency access" },
|
||||
{ key: "breachMonitoring", value: "Breach monitoring" },
|
||||
{ key: "andMoreFeatures", value: "And more!" },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
title: "Billing/Premium Upgrade Dialog",
|
||||
component: PremiumUpgradeDialogComponent,
|
||||
@@ -86,11 +103,11 @@ export default {
|
||||
t: (key: string) => {
|
||||
switch (key) {
|
||||
case "upgradeNow":
|
||||
return "Upgrade Now";
|
||||
return "Upgrade now";
|
||||
case "month":
|
||||
return "month";
|
||||
case "upgradeToPremium":
|
||||
return "Upgrade To Premium";
|
||||
return "Upgrade to Premium";
|
||||
default:
|
||||
return key;
|
||||
}
|
||||
@@ -116,3 +133,18 @@ export default {
|
||||
|
||||
type Story = StoryObj<PremiumUpgradeDialogComponent>;
|
||||
export const Default: Story = {};
|
||||
|
||||
export const NoPricingData: Story = {
|
||||
decorators: [
|
||||
moduleMetadata({
|
||||
providers: [
|
||||
{
|
||||
provide: SubscriptionPricingServiceAbstraction,
|
||||
useValue: {
|
||||
getPersonalSubscriptionPricingTiers$: () => of([mockPremiumTierNoPricingData]),
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
@@ -3,12 +3,12 @@ import { CommonModule } from "@angular/common";
|
||||
import { ChangeDetectionStrategy, Component } from "@angular/core";
|
||||
import { catchError, EMPTY, firstValueFrom, map, Observable } from "rxjs";
|
||||
|
||||
import { SubscriptionPricingCardDetails } from "@bitwarden/angular/billing/types/subscription-pricing-card-details";
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { SubscriptionPricingServiceAbstraction } from "@bitwarden/common/billing/abstractions/subscription-pricing.service.abstraction";
|
||||
import {
|
||||
PersonalSubscriptionPricingTier,
|
||||
PersonalSubscriptionPricingTierIds,
|
||||
SubscriptionCadence,
|
||||
SubscriptionCadenceIds,
|
||||
} from "@bitwarden/common/billing/types/subscription-pricing-tier";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
@@ -16,7 +16,6 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import {
|
||||
ButtonModule,
|
||||
ButtonType,
|
||||
CenterPositionStrategy,
|
||||
DialogModule,
|
||||
DialogRef,
|
||||
@@ -27,14 +26,6 @@ import {
|
||||
} from "@bitwarden/components";
|
||||
import { LogService } from "@bitwarden/logging";
|
||||
|
||||
type CardDetails = {
|
||||
title: string;
|
||||
tagline: string;
|
||||
price: { amount: number; cadence: SubscriptionCadence };
|
||||
button: { text: string; type: ButtonType; icon?: { type: string; position: "before" | "after" } };
|
||||
features: string[];
|
||||
};
|
||||
|
||||
@Component({
|
||||
selector: "billing-premium-upgrade-dialog",
|
||||
standalone: true,
|
||||
@@ -51,9 +42,8 @@ type CardDetails = {
|
||||
templateUrl: "./premium-upgrade-dialog.component.html",
|
||||
})
|
||||
export class PremiumUpgradeDialogComponent {
|
||||
protected cardDetails$: Observable<CardDetails | null> = this.subscriptionPricingService
|
||||
.getPersonalSubscriptionPricingTiers$()
|
||||
.pipe(
|
||||
protected cardDetails$: Observable<SubscriptionPricingCardDetails | null> =
|
||||
this.subscriptionPricingService.getPersonalSubscriptionPricingTiers$().pipe(
|
||||
map((tiers) => tiers.find((tier) => tier.id === PersonalSubscriptionPricingTierIds.Premium)),
|
||||
map((tier) => this.mapPremiumTierToCardDetails(tier!)),
|
||||
catchError((error: unknown) => {
|
||||
@@ -91,14 +81,18 @@ export class PremiumUpgradeDialogComponent {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
|
||||
private mapPremiumTierToCardDetails(tier: PersonalSubscriptionPricingTier): CardDetails {
|
||||
private mapPremiumTierToCardDetails(
|
||||
tier: PersonalSubscriptionPricingTier,
|
||||
): SubscriptionPricingCardDetails {
|
||||
return {
|
||||
title: tier.name,
|
||||
tagline: tier.description,
|
||||
price: {
|
||||
amount: tier.passwordManager.annualPrice / 12,
|
||||
cadence: SubscriptionCadenceIds.Monthly,
|
||||
},
|
||||
price: tier.passwordManager.annualPrice
|
||||
? {
|
||||
amount: tier.passwordManager.annualPrice / 12,
|
||||
cadence: SubscriptionCadenceIds.Monthly,
|
||||
}
|
||||
: undefined,
|
||||
button: {
|
||||
text: this.i18nService.t("upgradeNow"),
|
||||
type: "primary",
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { SubscriptionCadence } from "@bitwarden/common/billing/types/subscription-pricing-tier";
|
||||
import { ButtonType } from "@bitwarden/components";
|
||||
|
||||
export type SubscriptionPricingCardDetails = {
|
||||
title: string;
|
||||
tagline: string;
|
||||
price?: { amount: number; cadence: SubscriptionCadence };
|
||||
button: { text: string; type: ButtonType; icon?: { type: string; position: "before" | "after" } };
|
||||
features: string[];
|
||||
};
|
||||
@@ -1498,7 +1498,13 @@ const safeProviders: SafeProvider[] = [
|
||||
safeProvider({
|
||||
provide: SubscriptionPricingServiceAbstraction,
|
||||
useClass: DefaultSubscriptionPricingService,
|
||||
deps: [BillingApiServiceAbstraction, ConfigService, I18nServiceAbstraction, LogService],
|
||||
deps: [
|
||||
BillingApiServiceAbstraction,
|
||||
ConfigService,
|
||||
I18nServiceAbstraction,
|
||||
LogService,
|
||||
EnvironmentService,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: OrganizationManagementPreferencesService,
|
||||
|
||||
Reference in New Issue
Block a user