[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:
parent
1940256fe2
commit
6c1d74a4ce
|
@ -27,18 +27,22 @@ 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";
|
||||
import { PopOutComponent } from "../../../../../platform/popup/components/pop-out.component";
|
||||
|
||||
import { PopupFooterComponent } from "./../../../../../platform/popup/layout/popup-footer.component";
|
||||
import { PopupHeaderComponent } from "./../../../../../platform/popup/layout/popup-header.component";
|
||||
import { PopupPageComponent } from "./../../../../../platform/popup/layout/popup-page.component";
|
||||
import { VaultPopupAutofillService } from "./../../../services/vault-popup-autofill.service";
|
||||
import { PopupFooterComponent } from "../../../../../platform/popup/layout/popup-footer.component";
|
||||
import { PopupHeaderComponent } from "../../../../../platform/popup/layout/popup-header.component";
|
||||
import { PopupPageComponent } from "../../../../../platform/popup/layout/popup-page.component";
|
||||
import { BrowserPremiumUpgradePromptService } from "../../../services/browser-premium-upgrade-prompt.service";
|
||||
import { VaultPopupAutofillService } from "../../../services/vault-popup-autofill.service";
|
||||
|
||||
@Component({
|
||||
selector: "app-view-v2",
|
||||
templateUrl: "view-v2.component.html",
|
||||
standalone: true,
|
||||
providers: [
|
||||
{ provide: PremiumUpgradePromptService, useClass: BrowserPremiumUpgradePromptService },
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
SearchModule,
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
import { TestBed } from "@angular/core/testing";
|
||||
import { Router } from "@angular/router";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
|
||||
import { BrowserPremiumUpgradePromptService } from "./browser-premium-upgrade-prompt.service";
|
||||
|
||||
describe("BrowserPremiumUpgradePromptService", () => {
|
||||
let service: BrowserPremiumUpgradePromptService;
|
||||
let router: MockProxy<Router>;
|
||||
|
||||
beforeEach(async () => {
|
||||
router = mock<Router>();
|
||||
await TestBed.configureTestingModule({
|
||||
providers: [BrowserPremiumUpgradePromptService, { provide: Router, useValue: router }],
|
||||
}).compileComponents();
|
||||
|
||||
service = TestBed.inject(BrowserPremiumUpgradePromptService);
|
||||
});
|
||||
|
||||
describe("promptForPremium", () => {
|
||||
it("navigates to the premium update screen", async () => {
|
||||
await service.promptForPremium();
|
||||
expect(router.navigate).toHaveBeenCalledWith(["/premium"]);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,18 @@
|
|||
import { inject } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
|
||||
import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service";
|
||||
|
||||
/**
|
||||
* This class handles the premium upgrade process for the browser extension.
|
||||
*/
|
||||
export class BrowserPremiumUpgradePromptService implements PremiumUpgradePromptService {
|
||||
private router = inject(Router);
|
||||
|
||||
async promptForPremium() {
|
||||
/**
|
||||
* Navigate to the premium update screen.
|
||||
*/
|
||||
await this.router.navigate(["/premium"]);
|
||||
}
|
||||
}
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
/**
|
||||
* This interface defines the a contract for a service that prompts the user to upgrade to premium.
|
||||
* It ensures that PremiumUpgradePromptService contains a promptForPremium method.
|
||||
*/
|
||||
export abstract class PremiumUpgradePromptService {
|
||||
abstract promptForPremium(organizationId?: string): Promise<void>;
|
||||
}
|
|
@ -97,7 +97,7 @@
|
|||
bitBadge
|
||||
variant="success"
|
||||
class="tw-ml-2 tw-cursor-pointer"
|
||||
(click)="getPremium()"
|
||||
(click)="getPremium(cipher.organizationId)"
|
||||
slot="end"
|
||||
>
|
||||
{{ "premium" | i18n }}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { CommonModule, DatePipe } from "@angular/common";
|
||||
import { Component, inject, Input } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
import { Observable, shareReplay } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
|
@ -20,6 +19,7 @@ import {
|
|||
ColorPasswordModule,
|
||||
} from "@bitwarden/components";
|
||||
|
||||
import { PremiumUpgradePromptService } from "../../../../../libs/common/src/vault/abstractions/premium-upgrade-prompt.service";
|
||||
import { BitTotpCountdownComponent } from "../../components/totp-countdown/totp-countdown.component";
|
||||
import { ReadOnlyCipherCardComponent } from "../read-only-cipher-card/read-only-cipher-card.component";
|
||||
|
||||
|
@ -61,8 +61,8 @@ export class LoginCredentialsViewComponent {
|
|||
|
||||
constructor(
|
||||
private billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||
private router: Router,
|
||||
private i18nService: I18nService,
|
||||
private premiumUpgradeService: PremiumUpgradePromptService,
|
||||
private eventCollectionService: EventCollectionService,
|
||||
) {}
|
||||
|
||||
|
@ -75,8 +75,8 @@ export class LoginCredentialsViewComponent {
|
|||
return `${dateCreated} ${creationDate}`;
|
||||
}
|
||||
|
||||
async getPremium() {
|
||||
await this.router.navigate(["/premium"]);
|
||||
async getPremium(organizationId?: string) {
|
||||
await this.premiumUpgradeService.promptForPremium(organizationId);
|
||||
}
|
||||
|
||||
async pwToggleValue(passwordVisible: boolean) {
|
||||
|
|
Loading…
Reference in New Issue