diff --git a/apps/web/src/app/oss-routing.module.ts b/apps/web/src/app/oss-routing.module.ts index 8e2d770f1e4..4db6e50bc6d 100644 --- a/apps/web/src/app/oss-routing.module.ts +++ b/apps/web/src/app/oss-routing.module.ts @@ -50,6 +50,7 @@ import { import { canAccessEmergencyAccess } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { AnonLayoutWrapperComponent, AnonLayoutWrapperData } from "@bitwarden/components"; import { LockComponent } from "@bitwarden/key-management-ui"; +import { premiumInterestRedirectGuard } from "@bitwarden/web-vault/app/vault/guards/premium-interest-redirect/premium-interest-redirect.guard"; import { flagEnabled, Flags } from "../utils/flags"; @@ -630,7 +631,7 @@ const routes: Routes = [ children: [ { path: "vault", - canActivate: [setupExtensionRedirectGuard], + canActivate: [premiumInterestRedirectGuard, setupExtensionRedirectGuard], loadChildren: () => VaultModule, }, { diff --git a/apps/web/src/app/vault/guards/premium-interest-redirect/premium-interest-redirect.guard.spec.ts b/apps/web/src/app/vault/guards/premium-interest-redirect/premium-interest-redirect.guard.spec.ts new file mode 100644 index 00000000000..f0f3af47150 --- /dev/null +++ b/apps/web/src/app/vault/guards/premium-interest-redirect/premium-interest-redirect.guard.spec.ts @@ -0,0 +1,88 @@ +import { TestBed } from "@angular/core/testing"; +import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from "@angular/router"; +import { BehaviorSubject } from "rxjs"; + +import { PremiumInterestStateService } from "@bitwarden/angular/billing/services/premium-interest/premium-interest-state.service.abstraction"; +import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; + +import { premiumInterestRedirectGuard } from "./premium-interest-redirect.guard"; + +describe("premiumInterestRedirectGuard", () => { + const _state = Object.freeze({}) as RouterStateSnapshot; + const emptyRoute = Object.freeze({ queryParams: {} }) as ActivatedRouteSnapshot; + + const account = { + id: "account-id", + } as Account; + + const activeAccount$ = new BehaviorSubject(account); + const createUrlTree = jest.fn(); + const getPremiumInterest = jest.fn().mockResolvedValue(false); + const logError = jest.fn(); + + beforeEach(() => { + getPremiumInterest.mockClear(); + createUrlTree.mockClear(); + logError.mockClear(); + activeAccount$.next(account); + + TestBed.configureTestingModule({ + providers: [ + { provide: Router, useValue: { createUrlTree } }, + { provide: AccountService, useValue: { activeAccount$ } }, + { + provide: PremiumInterestStateService, + useValue: { getPremiumInterest }, + }, + { provide: LogService, useValue: { error: logError } }, + ], + }); + }); + + function runPremiumInterestGuard(route?: ActivatedRouteSnapshot) { + // Run the guard within injection context so `inject` works as you'd expect + // Pass state object to make TypeScript happy + return TestBed.runInInjectionContext(async () => + premiumInterestRedirectGuard(route ?? emptyRoute, _state), + ); + } + + it("returns `true` when the user does not intend to setup premium", async () => { + getPremiumInterest.mockResolvedValueOnce(false); + + expect(await runPremiumInterestGuard()).toBe(true); + }); + + it("redirects to premium subscription page when user intends to setup premium", async () => { + const urlTree = { toString: () => "/settings/subscription/premium" }; + createUrlTree.mockReturnValueOnce(urlTree); + getPremiumInterest.mockResolvedValueOnce(true); + + const result = await runPremiumInterestGuard(); + + expect(createUrlTree).toHaveBeenCalledWith(["/settings/subscription/premium"], { + queryParams: { callToAction: "upgradeToPremium" }, + }); + expect(result).toBe(urlTree); + }); + + it("redirects to login when active account is missing", async () => { + const urlTree = { toString: () => "/login" }; + createUrlTree.mockReturnValueOnce(urlTree); + activeAccount$.next(null); + + const result = await runPremiumInterestGuard(); + + expect(createUrlTree).toHaveBeenCalledWith(["/login"]); + expect(result).toBe(urlTree); + }); + + it("returns `true` and logs error when getPremiumInterest throws an error", async () => { + const error = new Error("Premium interest check failed"); + getPremiumInterest.mockRejectedValueOnce(error); + + expect(await runPremiumInterestGuard()).toBe(true); + expect(logError).toHaveBeenCalledWith("Error in premiumInterestRedirectGuard", error); + }); +}); diff --git a/apps/web/src/app/vault/guards/premium-interest-redirect/premium-interest-redirect.guard.ts b/apps/web/src/app/vault/guards/premium-interest-redirect/premium-interest-redirect.guard.ts new file mode 100644 index 00000000000..0fb0d744304 --- /dev/null +++ b/apps/web/src/app/vault/guards/premium-interest-redirect/premium-interest-redirect.guard.ts @@ -0,0 +1,37 @@ +import { inject } from "@angular/core"; +import { CanActivateFn, Router } from "@angular/router"; +import { firstValueFrom } from "rxjs"; + +import { PremiumInterestStateService } from "@bitwarden/angular/billing/services/premium-interest/premium-interest-state.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; + +export const premiumInterestRedirectGuard: CanActivateFn = async () => { + const router = inject(Router); + const accountService = inject(AccountService); + const premiumInterestStateService = inject(PremiumInterestStateService); + const logService = inject(LogService); + + try { + const currentAcct = await firstValueFrom(accountService.activeAccount$); + + if (!currentAcct) { + return router.createUrlTree(["/login"]); + } + + const intendsToSetupPremium = await premiumInterestStateService.getPremiumInterest( + currentAcct.id, + ); + + if (intendsToSetupPremium) { + return router.createUrlTree(["/settings/subscription/premium"], { + queryParams: { callToAction: "upgradeToPremium" }, + }); + } + + return true; + } catch (error) { + logService.error("Error in premiumInterestRedirectGuard", error); + return true; + } +};