mirror of
https://github.com/bitwarden/browser
synced 2025-12-15 15:53:27 +00:00
[PM-11395] [Defect] View Login - TOTP premium badge does nothing when clicked (#10857)
* Add MessagingService to LoginCredentialView component. * Add comments. * Add WIP PremiumUpgradeService * Simplify web PremiumUpgradeServices into one service. * Relocate service files. * Add browser version of PremiumUpgradePromptService. * Cleanup debug comments. * Run prettier. * rework promptForPremium to take organization id and add test. * Add test for browser * Rework imports to fix linter errors. * Add Shane's reworked WebVaultPremiumUpgradePromptService.
This commit is contained in:
@@ -188,7 +188,7 @@ export class AppComponent implements OnDestroy, OnInit {
|
||||
if (premiumConfirmed) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.router.navigate(["settings/subscription/premium"]);
|
||||
await this.router.navigate(["settings/subscription/premium"]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, EventEmitter, Inject, OnDestroy, OnInit } from "@angular/core";
|
||||
import { Component, Inject, OnInit, EventEmitter, OnDestroy } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
import { Subject } from "rxjs";
|
||||
|
||||
@@ -19,8 +19,10 @@ import {
|
||||
ToastService,
|
||||
} from "@bitwarden/components";
|
||||
|
||||
import { PremiumUpgradePromptService } from "../../../../../../libs/common/src/vault/abstractions/premium-upgrade-prompt.service";
|
||||
import { CipherViewComponent } from "../../../../../../libs/vault/src/cipher-view/cipher-view.component";
|
||||
import { SharedModule } from "../../shared/shared.module";
|
||||
import { WebVaultPremiumUpgradePromptService } from "../services/web-premium-upgrade-prompt.service";
|
||||
|
||||
export interface ViewCipherDialogParams {
|
||||
cipher: CipherView;
|
||||
@@ -29,6 +31,7 @@ export interface ViewCipherDialogParams {
|
||||
export enum ViewCipherDialogResult {
|
||||
Edited = "edited",
|
||||
Deleted = "deleted",
|
||||
PremiumUpgrade = "premiumUpgrade",
|
||||
}
|
||||
|
||||
export interface ViewCipherDialogCloseResult {
|
||||
@@ -43,6 +46,9 @@ export interface ViewCipherDialogCloseResult {
|
||||
templateUrl: "view.component.html",
|
||||
standalone: true,
|
||||
imports: [CipherViewComponent, CommonModule, AsyncActionsModule, DialogModule, SharedModule],
|
||||
providers: [
|
||||
{ provide: PremiumUpgradePromptService, useClass: WebVaultPremiumUpgradePromptService },
|
||||
],
|
||||
})
|
||||
export class ViewComponent implements OnInit, OnDestroy {
|
||||
cipher: CipherView;
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
import { DialogRef } from "@angular/cdk/dialog";
|
||||
import { TestBed } from "@angular/core/testing";
|
||||
import { Router } from "@angular/router";
|
||||
import { of, lastValueFrom } from "rxjs";
|
||||
|
||||
import { OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
import {
|
||||
ViewCipherDialogCloseResult,
|
||||
ViewCipherDialogResult,
|
||||
} from "../individual-vault/view.component";
|
||||
|
||||
import { WebVaultPremiumUpgradePromptService } from "./web-premium-upgrade-prompt.service";
|
||||
|
||||
describe("WebVaultPremiumUpgradePromptService", () => {
|
||||
let service: WebVaultPremiumUpgradePromptService;
|
||||
let dialogServiceMock: jest.Mocked<DialogService>;
|
||||
let routerMock: jest.Mocked<Router>;
|
||||
let dialogRefMock: jest.Mocked<DialogRef<ViewCipherDialogCloseResult>>;
|
||||
|
||||
beforeEach(() => {
|
||||
dialogServiceMock = {
|
||||
openSimpleDialog: jest.fn(),
|
||||
} as unknown as jest.Mocked<DialogService>;
|
||||
|
||||
routerMock = {
|
||||
navigate: jest.fn(),
|
||||
} as unknown as jest.Mocked<Router>;
|
||||
|
||||
dialogRefMock = {
|
||||
close: jest.fn(),
|
||||
} as unknown as jest.Mocked<DialogRef<ViewCipherDialogCloseResult>>;
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
WebVaultPremiumUpgradePromptService,
|
||||
{ provide: DialogService, useValue: dialogServiceMock },
|
||||
{ provide: Router, useValue: routerMock },
|
||||
{ provide: DialogRef, useValue: dialogRefMock },
|
||||
],
|
||||
});
|
||||
|
||||
service = TestBed.inject(WebVaultPremiumUpgradePromptService);
|
||||
});
|
||||
|
||||
it("prompts for premium upgrade and navigates to organization billing if organizationId is provided", async () => {
|
||||
dialogServiceMock.openSimpleDialog.mockReturnValue(lastValueFrom(of(true)));
|
||||
const organizationId = "test-org-id" as OrganizationId;
|
||||
|
||||
await service.promptForPremium(organizationId);
|
||||
|
||||
expect(dialogServiceMock.openSimpleDialog).toHaveBeenCalledWith({
|
||||
title: { key: "upgradeOrganization" },
|
||||
content: { key: "upgradeOrganizationDesc" },
|
||||
acceptButtonText: { key: "upgradeOrganization" },
|
||||
type: "info",
|
||||
});
|
||||
expect(routerMock.navigate).toHaveBeenCalledWith([
|
||||
"organizations",
|
||||
organizationId,
|
||||
"billing",
|
||||
"subscription",
|
||||
]);
|
||||
expect(dialogRefMock.close).toHaveBeenCalledWith({
|
||||
action: ViewCipherDialogResult.PremiumUpgrade,
|
||||
});
|
||||
});
|
||||
|
||||
it("prompts for premium upgrade and navigates to premium subscription if organizationId is not provided", async () => {
|
||||
dialogServiceMock.openSimpleDialog.mockReturnValue(lastValueFrom(of(true)));
|
||||
|
||||
await service.promptForPremium();
|
||||
|
||||
expect(dialogServiceMock.openSimpleDialog).toHaveBeenCalledWith({
|
||||
title: { key: "premiumRequired" },
|
||||
content: { key: "premiumRequiredDesc" },
|
||||
acceptButtonText: { key: "upgrade" },
|
||||
type: "success",
|
||||
});
|
||||
expect(routerMock.navigate).toHaveBeenCalledWith(["settings/subscription/premium"]);
|
||||
expect(dialogRefMock.close).toHaveBeenCalledWith({
|
||||
action: ViewCipherDialogResult.PremiumUpgrade,
|
||||
});
|
||||
});
|
||||
|
||||
it("does not navigate or close dialog if upgrade is no action is taken", async () => {
|
||||
dialogServiceMock.openSimpleDialog.mockReturnValue(lastValueFrom(of(false)));
|
||||
|
||||
await service.promptForPremium("test-org-id" as OrganizationId);
|
||||
|
||||
expect(routerMock.navigate).not.toHaveBeenCalled();
|
||||
expect(dialogRefMock.close).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,57 @@
|
||||
import { DialogRef } from "@angular/cdk/dialog";
|
||||
import { Injectable } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
|
||||
import { OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
import {
|
||||
ViewCipherDialogCloseResult,
|
||||
ViewCipherDialogResult,
|
||||
} from "../individual-vault/view.component";
|
||||
|
||||
/**
|
||||
* This service is used to prompt the user to upgrade to premium.
|
||||
*/
|
||||
@Injectable()
|
||||
export class WebVaultPremiumUpgradePromptService implements PremiumUpgradePromptService {
|
||||
constructor(
|
||||
private dialogService: DialogService,
|
||||
private router: Router,
|
||||
private dialog: DialogRef<ViewCipherDialogCloseResult>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Prompts the user to upgrade to premium.
|
||||
* @param organizationId The ID of the organization to upgrade.
|
||||
*/
|
||||
async promptForPremium(organizationId?: OrganizationId) {
|
||||
let upgradeConfirmed;
|
||||
if (organizationId) {
|
||||
upgradeConfirmed = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "upgradeOrganization" },
|
||||
content: { key: "upgradeOrganizationDesc" },
|
||||
acceptButtonText: { key: "upgradeOrganization" },
|
||||
type: "info",
|
||||
});
|
||||
if (upgradeConfirmed) {
|
||||
await this.router.navigate(["organizations", organizationId, "billing", "subscription"]);
|
||||
}
|
||||
} else {
|
||||
upgradeConfirmed = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "premiumRequired" },
|
||||
content: { key: "premiumRequiredDesc" },
|
||||
acceptButtonText: { key: "upgrade" },
|
||||
type: "success",
|
||||
});
|
||||
if (upgradeConfirmed) {
|
||||
await this.router.navigate(["settings/subscription/premium"]);
|
||||
}
|
||||
}
|
||||
|
||||
if (upgradeConfirmed) {
|
||||
this.dialog.close({ action: ViewCipherDialogResult.PremiumUpgrade });
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user