1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-16 16:23:44 +00:00

(Vault) [PM-27543] Create PremiumSetupIntentRedirectGuard (#17143)

* add state definition

* create abstraction, no-op, and web service

* update service name to reflect file name

* create redirect guard and add to web route

* update service import

* [PM-27543] Cleanup premiumInterestRedirectGuard

* [PM-27543] Add tests for premium-interest-redirect guard

* [PM-27543] Undo change to billing docs

* [PM-27543] Add error handling to guard

* [PM-27543] Improve tests

* [PM-27543] Add callToAction query parameter

---------

Co-authored-by: Shane <smelton@bitwarden.com>
This commit is contained in:
rr-bw
2025-11-05 11:23:14 -08:00
committed by GitHub
parent f53f3516b7
commit b65e02977e
3 changed files with 127 additions and 1 deletions

View File

@@ -50,6 +50,7 @@ import {
import { canAccessEmergencyAccess } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { canAccessEmergencyAccess } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { AnonLayoutWrapperComponent, AnonLayoutWrapperData } from "@bitwarden/components"; import { AnonLayoutWrapperComponent, AnonLayoutWrapperData } from "@bitwarden/components";
import { LockComponent } from "@bitwarden/key-management-ui"; 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"; import { flagEnabled, Flags } from "../utils/flags";
@@ -630,7 +631,7 @@ const routes: Routes = [
children: [ children: [
{ {
path: "vault", path: "vault",
canActivate: [setupExtensionRedirectGuard], canActivate: [premiumInterestRedirectGuard, setupExtensionRedirectGuard],
loadChildren: () => VaultModule, loadChildren: () => VaultModule,
}, },
{ {

View File

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

View File

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