1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-20 18:23:31 +00:00

[PM-23713] premium badge interaction (#16911)

* feature flag

* new upgrade dialog component and moved pricing service into libs

first draft

* moved pricing service to libs/common

removed toast service from the pricing service and implemented error handling in calling components

# Conflicts:
#	apps/web/src/app/billing/individual/upgrade/upgrade-payment/upgrade-payment.component.ts

* moved new premium upgrade dialog component to libs/angular

* badge opens new dialog in browser extension

* adds new dialog to desktop and fixes tests

* updates send dropdown to use premium prompt service

* styling and copy updates

* implement in web and desktop

* unit tests

* converting premium reports to use premium badge, and some cleanup

* fixes issue after merge

* linter errors

* pr feedback

* handle async promise correctly

* full sync after the premium upgrade is complete

* fixing test

* add padding to bottom of card in new dialog

* add support for self hosting

* fixing tests

* fix test

* Update has-premium.guard.ts

* pr feedback

* fix build and pr feedback

* fix build

* prettier

* fixing stories and making badge line height consistent

* pr feedback

* updated upgrade dialog to no longer use pricing card

* fixing incorrect markup and removing unused bits

* formatting

* pr feedback

removing unused message keys and adding back in code that was erroneously removed

* change detection

* close dialog when error

* claude pr feedback
This commit is contained in:
Kyle Denney
2025-11-03 10:16:01 -06:00
committed by GitHub
parent 3c16547f11
commit e1e3966cc2
55 changed files with 1462 additions and 355 deletions

View File

@@ -1523,12 +1523,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"
},
@@ -5772,6 +5766,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."

View File

@@ -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", () => {

View File

@@ -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";
@@ -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(
@@ -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;
}

View File

@@ -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();
});
});
});

View File

@@ -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"]);
}
}
}

View File

@@ -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;