Skip to content
Merged
Show file tree
Hide file tree
Changes from 47 commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
c0fce75
feature flag
kdenney Oct 8, 2025
3b4e33d
new upgrade dialog component and moved pricing service into libs
kdenney Oct 8, 2025
d1be2ea
moved pricing service to libs/common
kdenney Oct 9, 2025
10cd5d4
moved new premium upgrade dialog component to libs/angular
kdenney Oct 9, 2025
31f49b0
badge opens new dialog in browser extension
kdenney Oct 9, 2025
eff96f9
adds new dialog to desktop and fixes tests
kdenney Oct 13, 2025
86c72a3
updates send dropdown to use premium prompt service
kdenney Oct 13, 2025
9d039ee
styling and copy updates
kdenney Oct 14, 2025
c6b2830
implement in web and desktop
kdenney Oct 16, 2025
54c5b75
unit tests
kdenney Oct 16, 2025
93be319
converting premium reports to use premium badge, and some cleanup
kdenney Oct 16, 2025
5f240fb
Merge branch 'main' into billing/pm-23713/premium-badge-interaction
kdenney Oct 16, 2025
b5164a4
fixes issue after merge
kdenney Oct 16, 2025
6549c37
linter errors
kdenney Oct 17, 2025
3a254d1
pr feedback
kdenney Oct 17, 2025
54664d3
Merge branch 'main' into billing/pm-23713/premium-badge-interaction
kdenney Oct 17, 2025
09e0359
handle async promise correctly
kdenney Oct 17, 2025
16968bf
full sync after the premium upgrade is complete
kdenney Oct 17, 2025
c3f487d
fixing test
kdenney Oct 17, 2025
7dad7fe
Merge branch 'main' into billing/pm-23713/premium-badge-interaction
kdenney Oct 20, 2025
4b62463
add padding to bottom of card in new dialog
kdenney Oct 20, 2025
a02db7d
Merge branch 'main' into billing/pm-23713/premium-badge-interaction
kdenney Oct 21, 2025
d128be4
add support for self hosting
kdenney Oct 21, 2025
e2bfc94
Merge branch 'main' into billing/pm-23713/premium-badge-interaction
kdenney Oct 21, 2025
aa4d33c
fixing tests
kdenney Oct 21, 2025
812c380
Merge branch 'main' into billing/pm-23713/premium-badge-interaction
kdenney Oct 21, 2025
6112e78
fix test
kdenney Oct 21, 2025
ab137b5
Update has-premium.guard.ts
kdenney Oct 23, 2025
462d5dd
pr feedback
kdenney Oct 23, 2025
e2979c0
Merge branch 'main' into billing/pm-23713/premium-badge-interaction
kdenney Oct 23, 2025
fd089ca
fix build and pr feedback
kdenney Oct 23, 2025
e02231f
fix build
kdenney Oct 23, 2025
99791d7
Merge branch 'main' into billing/pm-23713/premium-badge-interactionc
kdenney Oct 23, 2025
8da526b
prettier
kdenney Oct 23, 2025
08df87d
fixing stories and making badge line height consistent
kdenney Oct 23, 2025
447ad49
pr feedback
kdenney Oct 24, 2025
c17a111
Merge branch 'main' into billing/pm-23713/premium-badge-interaction
kdenney Oct 24, 2025
3c7a3e0
Merge branch 'main' into billing/pm-23713/premium-badge-interaction
kdenney Oct 28, 2025
14d913e
updated upgrade dialog to no longer use pricing card
kdenney Oct 28, 2025
7ed43ca
Merge branch 'main' into billing/pm-23713/premium-badge-interaction
kdenney Oct 28, 2025
8892ca4
fixing incorrect markup and removing unused bits
kdenney Oct 28, 2025
723c387
Merge branch 'main' into billing/pm-23713/premium-badge-interaction
kdenney Oct 28, 2025
cd79c53
formatting
kdenney Oct 28, 2025
1c35ca3
Merge branch 'main' into billing/pm-23713/premium-badge-interaction
kdenney Oct 29, 2025
3373cec
pr feedback
kdenney Oct 29, 2025
7958b6c
change detection
kdenney Oct 29, 2025
b4d109d
Merge branch 'main' into billing/pm-23713/premium-badge-interaction
kdenney Oct 29, 2025
2abc88c
close dialog when error
kdenney Oct 30, 2025
2e33459
claude pr feedback
kdenney Oct 30, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 24 additions & 6 deletions apps/browser/src/_locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -1520,12 +1520,6 @@
"enableAutoBiometricsPrompt": {
"message": "Ask for biometrics on launch"
},
"premiumRequired": {
"message": "Premium required"
},
"premiumRequiredDesc": {
"message": "A Premium membership is required to use this feature."
},
"authenticationTimeout": {
"message": "Authentication timeout"
},
Expand Down Expand Up @@ -5766,6 +5760,30 @@
"atRiskLoginsSecured": {
"message": "Great job securing your at-risk logins!"
},
"upgradeNow": {
"message": "Upgrade now"
},
"builtInAuthenticator": {
"message": "Built-in authenticator"
},
"secureFileStorage": {
"message": "Secure file storage"
},
"emergencyAccess": {
"message": "Emergency access"
},
"breachMonitoring": {
"message": "Breach monitoring"
},
"andMoreFeatures": {
"message": "And more!"
},
"planDescPremium": {
"message": "Complete online security"
},
"upgradeToPremium": {
"message": "Upgrade to Premium"
},
"settingDisabledByPolicy": {
"message": "This setting is disabled by your organization's policy.",
"description": "This hint text is displayed when a user setting is disabled due to an organization policy."
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,11 +155,12 @@ describe("OpenAttachmentsComponent", () => {
});

it("routes the user to the premium page when they cannot access premium features", async () => {
const premiumUpgradeService = TestBed.inject(PremiumUpgradePromptService);
hasPremiumFromAnySource$.next(false);

await component.openAttachments();

expect(router.navigate).toHaveBeenCalledWith(["/premium"]);
expect(premiumUpgradeService.promptForPremium).toHaveBeenCalled();
});

it("disables attachments when the edit form is disabled", () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { ProductTierType } from "@bitwarden/common/billing/enums";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { CipherId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service";
import { BadgeModule, ItemModule, ToastService, TypographyModule } from "@bitwarden/components";
import { CipherFormContainer } from "@bitwarden/vault";

Expand Down Expand Up @@ -67,6 +68,7 @@ export class OpenAttachmentsComponent implements OnInit {
private filePopoutUtilsService: FilePopoutUtilsService,
private accountService: AccountService,
private cipherFormContainer: CipherFormContainer,
private premiumUpgradeService: PremiumUpgradePromptService,
) {
this.accountService.activeAccount$
.pipe(
Expand Down Expand Up @@ -115,7 +117,7 @@ export class OpenAttachmentsComponent implements OnInit {
/** Routes the user to the attachments screen, if available */
async openAttachments() {
if (!this.canAccessAttachments) {
await this.router.navigate(["/premium"]);
await this.premiumUpgradeService.promptForPremium();
return;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,69 @@ import { TestBed } from "@angular/core/testing";
import { Router } from "@angular/router";
import { mock, MockProxy } from "jest-mock-extended";

import { PremiumUpgradeDialogComponent } from "@bitwarden/angular/billing/components";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { DialogService } from "@bitwarden/components";

import { BrowserPremiumUpgradePromptService } from "./browser-premium-upgrade-prompt.service";

describe("BrowserPremiumUpgradePromptService", () => {
let service: BrowserPremiumUpgradePromptService;
let router: MockProxy<Router>;
let configService: MockProxy<ConfigService>;
let dialogService: MockProxy<DialogService>;

beforeEach(async () => {
router = mock<Router>();
configService = mock<ConfigService>();
dialogService = mock<DialogService>();

await TestBed.configureTestingModule({
providers: [BrowserPremiumUpgradePromptService, { provide: Router, useValue: router }],
providers: [
BrowserPremiumUpgradePromptService,
{ provide: Router, useValue: router },
{ provide: ConfigService, useValue: configService },
{ provide: DialogService, useValue: dialogService },
],
}).compileComponents();

service = TestBed.inject(BrowserPremiumUpgradePromptService);
});

describe("promptForPremium", () => {
it("navigates to the premium update screen", async () => {
let openSpy: jest.SpyInstance;

beforeEach(() => {
openSpy = jest.spyOn(PremiumUpgradeDialogComponent, "open").mockImplementation();
});

afterEach(() => {
openSpy.mockRestore();
});

it("opens the new premium upgrade dialog when feature flag is enabled", async () => {
configService.getFeatureFlag.mockResolvedValue(true);

await service.promptForPremium();

expect(configService.getFeatureFlag).toHaveBeenCalledWith(
FeatureFlag.PM23713_PremiumBadgeOpensNewPremiumUpgradeDialog,
);
expect(openSpy).toHaveBeenCalledWith(dialogService);
expect(router.navigate).not.toHaveBeenCalled();
});

it("navigates to the premium update screen when feature flag is disabled", async () => {
configService.getFeatureFlag.mockResolvedValue(false);

await service.promptForPremium();

expect(configService.getFeatureFlag).toHaveBeenCalledWith(
FeatureFlag.PM23713_PremiumBadgeOpensNewPremiumUpgradeDialog,
);
expect(router.navigate).toHaveBeenCalledWith(["/premium"]);
expect(openSpy).not.toHaveBeenCalled();
});
});
});
Original file line number Diff line number Diff line change
@@ -1,18 +1,32 @@
import { inject } from "@angular/core";
import { Router } from "@angular/router";

import { PremiumUpgradeDialogComponent } from "@bitwarden/angular/billing/components";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service";
import { DialogService } from "@bitwarden/components";

/**
* This class handles the premium upgrade process for the browser extension.
*/
export class BrowserPremiumUpgradePromptService implements PremiumUpgradePromptService {
private router = inject(Router);
private configService = inject(ConfigService);
private dialogService = inject(DialogService);

async promptForPremium() {
/**
* Navigate to the premium update screen.
*/
await this.router.navigate(["/premium"]);
const showNewDialog = await this.configService.getFeatureFlag(
FeatureFlag.PM23713_PremiumBadgeOpensNewPremiumUpgradeDialog,
);

if (showNewDialog) {
PremiumUpgradeDialogComponent.open(this.dialogService);
} else {
/**
* Navigate to the premium update screen.
*/
await this.router.navigate(["/premium"]);
}
}
}
1 change: 1 addition & 0 deletions apps/browser/tailwind.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ config.content = [
"../../libs/vault/src/**/*.{html,ts}",
"../../libs/angular/src/**/*.{html,ts}",
"../../libs/vault/src/**/*.{html,ts}",
"../../libs/pricing/src/**/*.{html,ts}",
];

module.exports = config;
11 changes: 11 additions & 0 deletions apps/desktop/src/app/tools/send/add-edit.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,23 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction";
import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service";
import { CalloutModule, DialogService, ToastService } from "@bitwarden/components";

import { DesktopPremiumUpgradePromptService } from "../../../services/desktop-premium-upgrade-prompt.service";

// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
@Component({
selector: "app-send-add-edit",
templateUrl: "add-edit.component.html",
imports: [CommonModule, JslibModule, ReactiveFormsModule, CalloutModule],
providers: [
{
provide: PremiumUpgradePromptService,
useClass: DesktopPremiumUpgradePromptService,
},
],
})
export class AddEditComponent extends BaseAddEditComponent {
constructor(
Expand All @@ -45,6 +54,7 @@ export class AddEditComponent extends BaseAddEditComponent {
billingAccountProfileStateService: BillingAccountProfileStateService,
accountService: AccountService,
toastService: ToastService,
premiumUpgradePromptService: PremiumUpgradePromptService,
) {
super(
i18nService,
Expand All @@ -62,6 +72,7 @@ export class AddEditComponent extends BaseAddEditComponent {
billingAccountProfileStateService,
accountService,
toastService,
premiumUpgradePromptService,
);
}

Expand Down
24 changes: 24 additions & 0 deletions apps/desktop/src/locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -4187,5 +4187,29 @@
},
"cardNumberLabel": {
"message": "Card number"
},
"upgradeNow": {
"message": "Upgrade now"
},
"builtInAuthenticator": {
"message": "Built-in authenticator"
},
"secureFileStorage": {
"message": "Secure file storage"
},
"emergencyAccess": {
"message": "Emergency access"
},
"breachMonitoring": {
"message": "Breach monitoring"
},
"andMoreFeatures": {
"message": "And more!"
},
"planDescPremium": {
"message": "Complete online security"
},
"upgradeToPremium": {
"message": "Upgrade to Premium"
}
}
Original file line number Diff line number Diff line change
@@ -1,30 +1,70 @@
import { TestBed } from "@angular/core/testing";
import { mock, MockProxy } from "jest-mock-extended";

import { PremiumUpgradeDialogComponent } from "@bitwarden/angular/billing/components";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { DialogService } from "@bitwarden/components";

import { DesktopPremiumUpgradePromptService } from "./desktop-premium-upgrade-prompt.service";

describe("DesktopPremiumUpgradePromptService", () => {
let service: DesktopPremiumUpgradePromptService;
let messager: MockProxy<MessagingService>;
let configService: MockProxy<ConfigService>;
let dialogService: MockProxy<DialogService>;

beforeEach(async () => {
messager = mock<MessagingService>();
configService = mock<ConfigService>();
dialogService = mock<DialogService>();

await TestBed.configureTestingModule({
providers: [
DesktopPremiumUpgradePromptService,
{ provide: MessagingService, useValue: messager },
{ provide: ConfigService, useValue: configService },
{ provide: DialogService, useValue: dialogService },
],
}).compileComponents();

service = TestBed.inject(DesktopPremiumUpgradePromptService);
});

describe("promptForPremium", () => {
it("navigates to the premium update screen", async () => {
let openSpy: jest.SpyInstance;

beforeEach(() => {
openSpy = jest.spyOn(PremiumUpgradeDialogComponent, "open").mockImplementation();
});

afterEach(() => {
openSpy.mockRestore();
});

it("opens the new premium upgrade dialog when feature flag is enabled", async () => {
configService.getFeatureFlag.mockResolvedValue(true);

await service.promptForPremium();

expect(configService.getFeatureFlag).toHaveBeenCalledWith(
FeatureFlag.PM23713_PremiumBadgeOpensNewPremiumUpgradeDialog,
);
expect(openSpy).toHaveBeenCalledWith(dialogService);
expect(messager.send).not.toHaveBeenCalled();
});

it("sends openPremium message when feature flag is disabled", async () => {
configService.getFeatureFlag.mockResolvedValue(false);

await service.promptForPremium();

expect(configService.getFeatureFlag).toHaveBeenCalledWith(
FeatureFlag.PM23713_PremiumBadgeOpensNewPremiumUpgradeDialog,
);
expect(messager.send).toHaveBeenCalledWith("openPremium");
expect(openSpy).not.toHaveBeenCalled();
});
});
});
Original file line number Diff line number Diff line change
@@ -1,15 +1,29 @@
import { inject } from "@angular/core";

import { PremiumUpgradeDialogComponent } from "@bitwarden/angular/billing/components";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service";
import { DialogService } from "@bitwarden/components";

/**
* This class handles the premium upgrade process for the desktop.
*/
export class DesktopPremiumUpgradePromptService implements PremiumUpgradePromptService {
private messagingService = inject(MessagingService);
private configService = inject(ConfigService);
private dialogService = inject(DialogService);

async promptForPremium() {
this.messagingService.send("openPremium");
const showNewDialog = await this.configService.getFeatureFlag(
FeatureFlag.PM23713_PremiumBadgeOpensNewPremiumUpgradeDialog,
);

if (showNewDialog) {
PremiumUpgradeDialogComponent.open(this.dialogService);
} else {
this.messagingService.send("openPremium");
}
}
}
1 change: 1 addition & 0 deletions apps/desktop/tailwind.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ config.content = [
"../../libs/key-management-ui/src/**/*.{html,ts}",
"../../libs/angular/src/**/*.{html,ts}",
"../../libs/vault/src/**/*.{html,ts,mdx}",
"../../libs/pricing/src/**/*.{html,ts}",
];

module.exports = config;
12 changes: 0 additions & 12 deletions apps/web/src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,18 +147,6 @@ export class AppComponent implements OnDestroy, OnInit {
}
break;
}
case "premiumRequired": {
const premiumConfirmed = await this.dialogService.openSimpleDialog({
title: { key: "premiumRequired" },
content: { key: "premiumRequiredDesc" },
acceptButtonText: { key: "upgrade" },
type: "success",
});
if (premiumConfirmed) {
await this.router.navigate(["settings/subscription/premium"]);
}
break;
}
case "emailVerificationRequired": {
const emailVerificationConfirmed = await this.dialogService.openSimpleDialog({
title: { key: "emailVerificationRequired" },
Expand Down
Loading
Loading