Skip to content

Commit e1e3966

Browse files
authored
[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
1 parent 3c16547 commit e1e3966

File tree

55 files changed

+1464
-357
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

55 files changed

+1464
-357
lines changed

apps/browser/src/_locales/en/messages.json

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1523,12 +1523,6 @@
15231523
"enableAutoBiometricsPrompt": {
15241524
"message": "Ask for biometrics on launch"
15251525
},
1526-
"premiumRequired": {
1527-
"message": "Premium required"
1528-
},
1529-
"premiumRequiredDesc": {
1530-
"message": "A Premium membership is required to use this feature."
1531-
},
15321526
"authenticationTimeout": {
15331527
"message": "Authentication timeout"
15341528
},
@@ -5772,6 +5766,30 @@
57725766
"atRiskLoginsSecured": {
57735767
"message": "Great job securing your at-risk logins!"
57745768
},
5769+
"upgradeNow": {
5770+
"message": "Upgrade now"
5771+
},
5772+
"builtInAuthenticator": {
5773+
"message": "Built-in authenticator"
5774+
},
5775+
"secureFileStorage": {
5776+
"message": "Secure file storage"
5777+
},
5778+
"emergencyAccess": {
5779+
"message": "Emergency access"
5780+
},
5781+
"breachMonitoring": {
5782+
"message": "Breach monitoring"
5783+
},
5784+
"andMoreFeatures": {
5785+
"message": "And more!"
5786+
},
5787+
"planDescPremium": {
5788+
"message": "Complete online security"
5789+
},
5790+
"upgradeToPremium": {
5791+
"message": "Upgrade to Premium"
5792+
},
57755793
"settingDisabledByPolicy": {
57765794
"message": "This setting is disabled by your organization's policy.",
57775795
"description": "This hint text is displayed when a user setting is disabled due to an organization policy."

apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.spec.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,11 +155,12 @@ describe("OpenAttachmentsComponent", () => {
155155
});
156156

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

160161
await component.openAttachments();
161162

162-
expect(router.navigate).toHaveBeenCalledWith(["/premium"]);
163+
expect(premiumUpgradeService.promptForPremium).toHaveBeenCalled();
163164
});
164165

165166
it("disables attachments when the edit form is disabled", () => {

apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { ProductTierType } from "@bitwarden/common/billing/enums";
1919
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
2020
import { CipherId } from "@bitwarden/common/types/guid";
2121
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
22+
import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service";
2223
import { BadgeModule, ItemModule, ToastService, TypographyModule } from "@bitwarden/components";
2324
import { CipherFormContainer } from "@bitwarden/vault";
2425

@@ -67,6 +68,7 @@ export class OpenAttachmentsComponent implements OnInit {
6768
private filePopoutUtilsService: FilePopoutUtilsService,
6869
private accountService: AccountService,
6970
private cipherFormContainer: CipherFormContainer,
71+
private premiumUpgradeService: PremiumUpgradePromptService,
7072
) {
7173
this.accountService.activeAccount$
7274
.pipe(
@@ -115,7 +117,7 @@ export class OpenAttachmentsComponent implements OnInit {
115117
/** Routes the user to the attachments screen, if available */
116118
async openAttachments() {
117119
if (!this.canAccessAttachments) {
118-
await this.router.navigate(["/premium"]);
120+
await this.premiumUpgradeService.promptForPremium();
119121
return;
120122
}
121123

apps/browser/src/vault/popup/services/browser-premium-upgrade-prompt.service.spec.ts

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,69 @@ import { TestBed } from "@angular/core/testing";
22
import { Router } from "@angular/router";
33
import { mock, MockProxy } from "jest-mock-extended";
44

5+
import { PremiumUpgradeDialogComponent } from "@bitwarden/angular/billing/components";
6+
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
7+
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
8+
import { DialogService } from "@bitwarden/components";
9+
510
import { BrowserPremiumUpgradePromptService } from "./browser-premium-upgrade-prompt.service";
611

712
describe("BrowserPremiumUpgradePromptService", () => {
813
let service: BrowserPremiumUpgradePromptService;
914
let router: MockProxy<Router>;
15+
let configService: MockProxy<ConfigService>;
16+
let dialogService: MockProxy<DialogService>;
1017

1118
beforeEach(async () => {
1219
router = mock<Router>();
20+
configService = mock<ConfigService>();
21+
dialogService = mock<DialogService>();
22+
1323
await TestBed.configureTestingModule({
14-
providers: [BrowserPremiumUpgradePromptService, { provide: Router, useValue: router }],
24+
providers: [
25+
BrowserPremiumUpgradePromptService,
26+
{ provide: Router, useValue: router },
27+
{ provide: ConfigService, useValue: configService },
28+
{ provide: DialogService, useValue: dialogService },
29+
],
1530
}).compileComponents();
1631

1732
service = TestBed.inject(BrowserPremiumUpgradePromptService);
1833
});
1934

2035
describe("promptForPremium", () => {
21-
it("navigates to the premium update screen", async () => {
36+
let openSpy: jest.SpyInstance;
37+
38+
beforeEach(() => {
39+
openSpy = jest.spyOn(PremiumUpgradeDialogComponent, "open").mockImplementation();
40+
});
41+
42+
afterEach(() => {
43+
openSpy.mockRestore();
44+
});
45+
46+
it("opens the new premium upgrade dialog when feature flag is enabled", async () => {
47+
configService.getFeatureFlag.mockResolvedValue(true);
48+
2249
await service.promptForPremium();
50+
51+
expect(configService.getFeatureFlag).toHaveBeenCalledWith(
52+
FeatureFlag.PM23713_PremiumBadgeOpensNewPremiumUpgradeDialog,
53+
);
54+
expect(openSpy).toHaveBeenCalledWith(dialogService);
55+
expect(router.navigate).not.toHaveBeenCalled();
56+
});
57+
58+
it("navigates to the premium update screen when feature flag is disabled", async () => {
59+
configService.getFeatureFlag.mockResolvedValue(false);
60+
61+
await service.promptForPremium();
62+
63+
expect(configService.getFeatureFlag).toHaveBeenCalledWith(
64+
FeatureFlag.PM23713_PremiumBadgeOpensNewPremiumUpgradeDialog,
65+
);
2366
expect(router.navigate).toHaveBeenCalledWith(["/premium"]);
67+
expect(openSpy).not.toHaveBeenCalled();
2468
});
2569
});
2670
});
Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,32 @@
11
import { inject } from "@angular/core";
22
import { Router } from "@angular/router";
33

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

610
/**
711
* This class handles the premium upgrade process for the browser extension.
812
*/
913
export class BrowserPremiumUpgradePromptService implements PremiumUpgradePromptService {
1014
private router = inject(Router);
15+
private configService = inject(ConfigService);
16+
private dialogService = inject(DialogService);
1117

1218
async promptForPremium() {
13-
/**
14-
* Navigate to the premium update screen.
15-
*/
16-
await this.router.navigate(["/premium"]);
19+
const showNewDialog = await this.configService.getFeatureFlag(
20+
FeatureFlag.PM23713_PremiumBadgeOpensNewPremiumUpgradeDialog,
21+
);
22+
23+
if (showNewDialog) {
24+
PremiumUpgradeDialogComponent.open(this.dialogService);
25+
} else {
26+
/**
27+
* Navigate to the premium update screen.
28+
*/
29+
await this.router.navigate(["/premium"]);
30+
}
1731
}
1832
}

apps/browser/tailwind.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ config.content = [
1010
"../../libs/vault/src/**/*.{html,ts}",
1111
"../../libs/angular/src/**/*.{html,ts}",
1212
"../../libs/vault/src/**/*.{html,ts}",
13+
"../../libs/pricing/src/**/*.{html,ts}",
1314
];
1415

1516
module.exports = config;

apps/desktop/src/app/tools/send/add-edit.component.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,23 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
1919
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
2020
import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction";
2121
import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
22+
import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service";
2223
import { CalloutModule, DialogService, ToastService } from "@bitwarden/components";
2324

25+
import { DesktopPremiumUpgradePromptService } from "../../../services/desktop-premium-upgrade-prompt.service";
26+
2427
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
2528
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
2629
@Component({
2730
selector: "app-send-add-edit",
2831
templateUrl: "add-edit.component.html",
2932
imports: [CommonModule, JslibModule, ReactiveFormsModule, CalloutModule],
33+
providers: [
34+
{
35+
provide: PremiumUpgradePromptService,
36+
useClass: DesktopPremiumUpgradePromptService,
37+
},
38+
],
3039
})
3140
export class AddEditComponent extends BaseAddEditComponent {
3241
constructor(
@@ -45,6 +54,7 @@ export class AddEditComponent extends BaseAddEditComponent {
4554
billingAccountProfileStateService: BillingAccountProfileStateService,
4655
accountService: AccountService,
4756
toastService: ToastService,
57+
premiumUpgradePromptService: PremiumUpgradePromptService,
4858
) {
4959
super(
5060
i18nService,
@@ -62,6 +72,7 @@ export class AddEditComponent extends BaseAddEditComponent {
6272
billingAccountProfileStateService,
6373
accountService,
6474
toastService,
75+
premiumUpgradePromptService,
6576
);
6677
}
6778

apps/desktop/src/locales/en/messages.json

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4193,5 +4193,29 @@
41934193
},
41944194
"cardNumberLabel": {
41954195
"message": "Card number"
4196+
},
4197+
"upgradeNow": {
4198+
"message": "Upgrade now"
4199+
},
4200+
"builtInAuthenticator": {
4201+
"message": "Built-in authenticator"
4202+
},
4203+
"secureFileStorage": {
4204+
"message": "Secure file storage"
4205+
},
4206+
"emergencyAccess": {
4207+
"message": "Emergency access"
4208+
},
4209+
"breachMonitoring": {
4210+
"message": "Breach monitoring"
4211+
},
4212+
"andMoreFeatures": {
4213+
"message": "And more!"
4214+
},
4215+
"planDescPremium": {
4216+
"message": "Complete online security"
4217+
},
4218+
"upgradeToPremium": {
4219+
"message": "Upgrade to Premium"
41964220
}
41974221
}
Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,70 @@
11
import { TestBed } from "@angular/core/testing";
22
import { mock, MockProxy } from "jest-mock-extended";
33

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

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

812
describe("DesktopPremiumUpgradePromptService", () => {
913
let service: DesktopPremiumUpgradePromptService;
1014
let messager: MockProxy<MessagingService>;
15+
let configService: MockProxy<ConfigService>;
16+
let dialogService: MockProxy<DialogService>;
1117

1218
beforeEach(async () => {
1319
messager = mock<MessagingService>();
20+
configService = mock<ConfigService>();
21+
dialogService = mock<DialogService>();
22+
1423
await TestBed.configureTestingModule({
1524
providers: [
1625
DesktopPremiumUpgradePromptService,
1726
{ provide: MessagingService, useValue: messager },
27+
{ provide: ConfigService, useValue: configService },
28+
{ provide: DialogService, useValue: dialogService },
1829
],
1930
}).compileComponents();
2031

2132
service = TestBed.inject(DesktopPremiumUpgradePromptService);
2233
});
2334

2435
describe("promptForPremium", () => {
25-
it("navigates to the premium update screen", async () => {
36+
let openSpy: jest.SpyInstance;
37+
38+
beforeEach(() => {
39+
openSpy = jest.spyOn(PremiumUpgradeDialogComponent, "open").mockImplementation();
40+
});
41+
42+
afterEach(() => {
43+
openSpy.mockRestore();
44+
});
45+
46+
it("opens the new premium upgrade dialog when feature flag is enabled", async () => {
47+
configService.getFeatureFlag.mockResolvedValue(true);
48+
2649
await service.promptForPremium();
50+
51+
expect(configService.getFeatureFlag).toHaveBeenCalledWith(
52+
FeatureFlag.PM23713_PremiumBadgeOpensNewPremiumUpgradeDialog,
53+
);
54+
expect(openSpy).toHaveBeenCalledWith(dialogService);
55+
expect(messager.send).not.toHaveBeenCalled();
56+
});
57+
58+
it("sends openPremium message when feature flag is disabled", async () => {
59+
configService.getFeatureFlag.mockResolvedValue(false);
60+
61+
await service.promptForPremium();
62+
63+
expect(configService.getFeatureFlag).toHaveBeenCalledWith(
64+
FeatureFlag.PM23713_PremiumBadgeOpensNewPremiumUpgradeDialog,
65+
);
2766
expect(messager.send).toHaveBeenCalledWith("openPremium");
67+
expect(openSpy).not.toHaveBeenCalled();
2868
});
2969
});
3070
});
Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,29 @@
11
import { inject } from "@angular/core";
22

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

610
/**
711
* This class handles the premium upgrade process for the desktop.
812
*/
913
export class DesktopPremiumUpgradePromptService implements PremiumUpgradePromptService {
1014
private messagingService = inject(MessagingService);
15+
private configService = inject(ConfigService);
16+
private dialogService = inject(DialogService);
1117

1218
async promptForPremium() {
13-
this.messagingService.send("openPremium");
19+
const showNewDialog = await this.configService.getFeatureFlag(
20+
FeatureFlag.PM23713_PremiumBadgeOpensNewPremiumUpgradeDialog,
21+
);
22+
23+
if (showNewDialog) {
24+
PremiumUpgradeDialogComponent.open(this.dialogService);
25+
} else {
26+
this.messagingService.send("openPremium");
27+
}
1428
}
1529
}

0 commit comments

Comments
 (0)