1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-11 22:03:36 +00:00

[PM-12034] Remove usage of ActiveUserState from vault-banners.service (#11543)

* Migrated banner service from using active user state

* Fixed unit tests for the vault banner service

* Updated component to pass user id required by the banner service

* Updated component tests

* Added comments

* Fixed unit tests

* Updated vault banner service to use lastSync$ version and removed polling

* Updated to use UserDecryptionOptions

* Updated to use getKdfConfig$

* Updated shouldShowVerifyEmailBanner to use account observable

* Added takewhile operator to only make calls when userId is present

* Simplified to use sing userId

* Simplified to use sing userId
This commit is contained in:
SmithThe4th
2025-01-09 13:12:08 -05:00
committed by GitHub
parent 06ca00f3c1
commit 14568f11dc
4 changed files with 153 additions and 143 deletions

View File

@@ -1,11 +1,14 @@
import { TestBed } from "@angular/core/testing"; import { TestBed } from "@angular/core/testing";
import { BehaviorSubject, firstValueFrom } from "rxjs"; import { BehaviorSubject, firstValueFrom, take, timeout } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import {
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; UserDecryptionOptions,
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; UserDecryptionOptionsServiceAbstraction,
} from "@bitwarden/auth/common";
import { AccountInfo, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { StateProvider } from "@bitwarden/common/platform/state"; import { StateProvider } from "@bitwarden/common/platform/state";
import { FakeStateProvider, mockAccountServiceWith } from "@bitwarden/common/spec"; import { FakeStateProvider, mockAccountServiceWith } from "@bitwarden/common/spec";
import { UserId } from "@bitwarden/common/types/guid"; import { UserId } from "@bitwarden/common/types/guid";
@@ -22,18 +25,20 @@ describe("VaultBannersService", () => {
let service: VaultBannersService; let service: VaultBannersService;
const isSelfHost = jest.fn().mockReturnValue(false); const isSelfHost = jest.fn().mockReturnValue(false);
const hasPremiumFromAnySource$ = new BehaviorSubject<boolean>(false); const hasPremiumFromAnySource$ = new BehaviorSubject<boolean>(false);
const userId = "user-id" as UserId; const userId = Utils.newGuid() as UserId;
const fakeStateProvider = new FakeStateProvider(mockAccountServiceWith(userId)); const fakeStateProvider = new FakeStateProvider(mockAccountServiceWith(userId));
const getEmailVerified = jest.fn().mockResolvedValue(true); const getEmailVerified = jest.fn().mockResolvedValue(true);
const hasMasterPassword = jest.fn().mockResolvedValue(true); const lastSync$ = new BehaviorSubject<Date | null>(null);
const getKdfConfig = jest const userDecryptionOptions$ = new BehaviorSubject<UserDecryptionOptions>({
.fn() hasMasterPassword: true,
.mockResolvedValue({ kdfType: KdfType.PBKDF2_SHA256, iterations: 600000 }); });
const getLastSync = jest.fn().mockResolvedValue(null); const kdfConfig$ = new BehaviorSubject({ kdfType: KdfType.PBKDF2_SHA256, iterations: 600000 });
const accounts$ = new BehaviorSubject<Record<UserId, AccountInfo>>({
[userId]: { email: "test@bitwarden.com", emailVerified: true, name: "name" } as AccountInfo,
});
beforeEach(() => { beforeEach(() => {
jest.useFakeTimers(); lastSync$.next(new Date("2024-05-14"));
getLastSync.mockClear().mockResolvedValue(new Date("2024-05-14"));
isSelfHost.mockClear(); isSelfHost.mockClear();
getEmailVerified.mockClear().mockResolvedValue(true); getEmailVerified.mockClear().mockResolvedValue(true);
@@ -52,25 +57,27 @@ describe("VaultBannersService", () => {
provide: StateProvider, provide: StateProvider,
useValue: fakeStateProvider, useValue: fakeStateProvider,
}, },
{
provide: PlatformUtilsService,
useValue: { isSelfHost },
},
{ {
provide: AccountService, provide: AccountService,
useValue: mockAccountServiceWith(userId), useValue: { accounts$ },
},
{
provide: TokenService,
useValue: { getEmailVerified },
},
{
provide: UserVerificationService,
useValue: { hasMasterPassword },
}, },
{ {
provide: KdfConfigService, provide: KdfConfigService,
useValue: { getKdfConfig }, useValue: { getKdfConfig$: () => kdfConfig$ },
}, },
{ {
provide: SyncService, provide: SyncService,
useValue: { getLastSync }, useValue: { lastSync$: () => lastSync$ },
},
{
provide: UserDecryptionOptionsServiceAbstraction,
useValue: {
userDecryptionOptionsById$: () => userDecryptionOptions$,
},
}, },
], ],
}); });
@@ -82,39 +89,38 @@ describe("VaultBannersService", () => {
describe("Premium", () => { describe("Premium", () => {
it("waits until sync is completed before showing premium banner", async () => { it("waits until sync is completed before showing premium banner", async () => {
getLastSync.mockResolvedValue(new Date("2024-05-14"));
hasPremiumFromAnySource$.next(false); hasPremiumFromAnySource$.next(false);
isSelfHost.mockReturnValue(false); isSelfHost.mockReturnValue(false);
lastSync$.next(null);
service = TestBed.inject(VaultBannersService); service = TestBed.inject(VaultBannersService);
jest.advanceTimersByTime(201); const premiumBanner$ = service.shouldShowPremiumBanner$(userId);
expect(await firstValueFrom(service.shouldShowPremiumBanner$)).toBe(true); // Should not emit when sync is null
await expect(firstValueFrom(premiumBanner$.pipe(take(1), timeout(100)))).rejects.toThrow();
// Should emit when sync is completed
lastSync$.next(new Date("2024-05-14"));
expect(await firstValueFrom(premiumBanner$)).toBe(true);
}); });
it("does not show a premium banner for self-hosted users", async () => { it("does not show a premium banner for self-hosted users", async () => {
getLastSync.mockResolvedValue(new Date("2024-05-14"));
hasPremiumFromAnySource$.next(false); hasPremiumFromAnySource$.next(false);
isSelfHost.mockReturnValue(true); isSelfHost.mockReturnValue(true);
service = TestBed.inject(VaultBannersService); service = TestBed.inject(VaultBannersService);
jest.advanceTimersByTime(201); expect(await firstValueFrom(service.shouldShowPremiumBanner$(userId))).toBe(false);
expect(await firstValueFrom(service.shouldShowPremiumBanner$)).toBe(false);
}); });
it("does not show a premium banner when they have access to premium", async () => { it("does not show a premium banner when they have access to premium", async () => {
getLastSync.mockResolvedValue(new Date("2024-05-14"));
hasPremiumFromAnySource$.next(true); hasPremiumFromAnySource$.next(true);
isSelfHost.mockReturnValue(false); isSelfHost.mockReturnValue(false);
service = TestBed.inject(VaultBannersService); service = TestBed.inject(VaultBannersService);
jest.advanceTimersByTime(201); expect(await firstValueFrom(service.shouldShowPremiumBanner$(userId))).toBe(false);
expect(await firstValueFrom(service.shouldShowPremiumBanner$)).toBe(false);
}); });
describe("dismissing", () => { describe("dismissing", () => {
@@ -125,7 +131,7 @@ describe("VaultBannersService", () => {
jest.setSystemTime(date.getTime()); jest.setSystemTime(date.getTime());
service = TestBed.inject(VaultBannersService); service = TestBed.inject(VaultBannersService);
await service.dismissBanner(VisibleVaultBanner.Premium); await service.dismissBanner(userId, VisibleVaultBanner.Premium);
}); });
afterEach(() => { afterEach(() => {
@@ -134,7 +140,7 @@ describe("VaultBannersService", () => {
it("updates state on first dismiss", async () => { it("updates state on first dismiss", async () => {
const state = await firstValueFrom( const state = await firstValueFrom(
fakeStateProvider.getActive(PREMIUM_BANNER_REPROMPT_KEY).state$, fakeStateProvider.getUser(userId, PREMIUM_BANNER_REPROMPT_KEY).state$,
); );
const oneWeekLater = new Date("2023-06-15"); const oneWeekLater = new Date("2023-06-15");
@@ -148,7 +154,7 @@ describe("VaultBannersService", () => {
it("updates state on second dismiss", async () => { it("updates state on second dismiss", async () => {
const state = await firstValueFrom( const state = await firstValueFrom(
fakeStateProvider.getActive(PREMIUM_BANNER_REPROMPT_KEY).state$, fakeStateProvider.getUser(userId, PREMIUM_BANNER_REPROMPT_KEY).state$,
); );
const oneMonthLater = new Date("2023-07-08"); const oneMonthLater = new Date("2023-07-08");
@@ -162,7 +168,7 @@ describe("VaultBannersService", () => {
it("updates state on third dismiss", async () => { it("updates state on third dismiss", async () => {
const state = await firstValueFrom( const state = await firstValueFrom(
fakeStateProvider.getActive(PREMIUM_BANNER_REPROMPT_KEY).state$, fakeStateProvider.getUser(userId, PREMIUM_BANNER_REPROMPT_KEY).state$,
); );
const oneYearLater = new Date("2024-06-08"); const oneYearLater = new Date("2024-06-08");
@@ -178,40 +184,40 @@ describe("VaultBannersService", () => {
describe("KDFSettings", () => { describe("KDFSettings", () => {
beforeEach(async () => { beforeEach(async () => {
hasMasterPassword.mockResolvedValue(true); userDecryptionOptions$.next({ hasMasterPassword: true });
getKdfConfig.mockResolvedValue({ kdfType: KdfType.PBKDF2_SHA256, iterations: 599999 }); kdfConfig$.next({ kdfType: KdfType.PBKDF2_SHA256, iterations: 599999 });
}); });
it("shows low KDF iteration banner", async () => { it("shows low KDF iteration banner", async () => {
service = TestBed.inject(VaultBannersService); service = TestBed.inject(VaultBannersService);
expect(await service.shouldShowLowKDFBanner()).toBe(true); expect(await service.shouldShowLowKDFBanner(userId)).toBe(true);
}); });
it("does not show low KDF iteration banner if KDF type is not PBKDF2_SHA256", async () => { it("does not show low KDF iteration banner if KDF type is not PBKDF2_SHA256", async () => {
getKdfConfig.mockResolvedValue({ kdfType: KdfType.Argon2id, iterations: 600001 }); kdfConfig$.next({ kdfType: KdfType.Argon2id, iterations: 600001 });
service = TestBed.inject(VaultBannersService); service = TestBed.inject(VaultBannersService);
expect(await service.shouldShowLowKDFBanner()).toBe(false); expect(await service.shouldShowLowKDFBanner(userId)).toBe(false);
}); });
it("does not show low KDF for iterations about 600,000", async () => { it("does not show low KDF for iterations about 600,000", async () => {
getKdfConfig.mockResolvedValue({ kdfType: KdfType.PBKDF2_SHA256, iterations: 600001 }); kdfConfig$.next({ kdfType: KdfType.PBKDF2_SHA256, iterations: 600001 });
service = TestBed.inject(VaultBannersService); service = TestBed.inject(VaultBannersService);
expect(await service.shouldShowLowKDFBanner()).toBe(false); expect(await service.shouldShowLowKDFBanner(userId)).toBe(false);
}); });
it("dismisses low KDF iteration banner", async () => { it("dismisses low KDF iteration banner", async () => {
service = TestBed.inject(VaultBannersService); service = TestBed.inject(VaultBannersService);
expect(await service.shouldShowLowKDFBanner()).toBe(true); expect(await service.shouldShowLowKDFBanner(userId)).toBe(true);
await service.dismissBanner(VisibleVaultBanner.KDFSettings); await service.dismissBanner(userId, VisibleVaultBanner.KDFSettings);
expect(await service.shouldShowLowKDFBanner()).toBe(false); expect(await service.shouldShowLowKDFBanner(userId)).toBe(false);
}); });
}); });
@@ -228,39 +234,44 @@ describe("VaultBannersService", () => {
it("shows outdated browser banner", async () => { it("shows outdated browser banner", async () => {
service = TestBed.inject(VaultBannersService); service = TestBed.inject(VaultBannersService);
expect(await service.shouldShowUpdateBrowserBanner()).toBe(true); expect(await service.shouldShowUpdateBrowserBanner(userId)).toBe(true);
}); });
it("dismisses outdated browser banner", async () => { it("dismisses outdated browser banner", async () => {
service = TestBed.inject(VaultBannersService); service = TestBed.inject(VaultBannersService);
expect(await service.shouldShowUpdateBrowserBanner()).toBe(true); expect(await service.shouldShowUpdateBrowserBanner(userId)).toBe(true);
await service.dismissBanner(VisibleVaultBanner.OutdatedBrowser); await service.dismissBanner(userId, VisibleVaultBanner.OutdatedBrowser);
expect(await service.shouldShowUpdateBrowserBanner()).toBe(false); expect(await service.shouldShowUpdateBrowserBanner(userId)).toBe(false);
}); });
}); });
describe("VerifyEmail", () => { describe("VerifyEmail", () => {
beforeEach(async () => { beforeEach(async () => {
getEmailVerified.mockResolvedValue(false); accounts$.next({
[userId]: {
...accounts$.value[userId],
emailVerified: false,
},
});
}); });
it("shows verify email banner", async () => { it("shows verify email banner", async () => {
service = TestBed.inject(VaultBannersService); service = TestBed.inject(VaultBannersService);
expect(await service.shouldShowVerifyEmailBanner()).toBe(true); expect(await service.shouldShowVerifyEmailBanner(userId)).toBe(true);
}); });
it("dismisses verify email banner", async () => { it("dismisses verify email banner", async () => {
service = TestBed.inject(VaultBannersService); service = TestBed.inject(VaultBannersService);
expect(await service.shouldShowVerifyEmailBanner()).toBe(true); expect(await service.shouldShowVerifyEmailBanner(userId)).toBe(true);
await service.dismissBanner(VisibleVaultBanner.VerifyEmail); await service.dismissBanner(userId, VisibleVaultBanner.VerifyEmail);
expect(await service.shouldShowVerifyEmailBanner()).toBe(false); expect(await service.shouldShowVerifyEmailBanner(userId)).toBe(false);
}); });
}); });
}); });

View File

@@ -1,28 +1,18 @@
import { Injectable } from "@angular/core"; import { Injectable } from "@angular/core";
import { import { Observable, combineLatest, firstValueFrom, map, filter, mergeMap, take } from "rxjs";
Subject,
Observable,
combineLatest,
firstValueFrom,
map,
mergeMap,
take,
switchMap,
of,
} from "rxjs";
import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { import {
StateProvider, StateProvider,
ActiveUserState,
PREMIUM_BANNER_DISK_LOCAL, PREMIUM_BANNER_DISK_LOCAL,
BANNERS_DISMISSED_DISK, BANNERS_DISMISSED_DISK,
UserKeyDefinition, UserKeyDefinition,
SingleUserState,
} from "@bitwarden/common/platform/state"; } from "@bitwarden/common/platform/state";
import { UserId } from "@bitwarden/common/types/guid";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { PBKDF2KdfConfig, KdfConfigService, KdfType } from "@bitwarden/key-management"; import { PBKDF2KdfConfig, KdfConfigService, KdfType } from "@bitwarden/key-management";
@@ -62,47 +52,25 @@ export const BANNERS_DISMISSED_DISK_KEY = new UserKeyDefinition<SessionBanners[]
@Injectable() @Injectable()
export class VaultBannersService { export class VaultBannersService {
shouldShowPremiumBanner$: Observable<boolean>;
private premiumBannerState: ActiveUserState<PremiumBannerReprompt>;
private sessionBannerState: ActiveUserState<SessionBanners[]>;
/**
* Emits when the sync service has completed a sync
*
* This is needed because `hasPremiumFromAnySource$` will emit false until the sync is completed
* resulting in the premium banner being shown briefly on startup when the user has access to
* premium features.
*/
private syncCompleted$ = new Subject<void>();
constructor( constructor(
private tokenService: TokenService, private accountService: AccountService,
private userVerificationService: UserVerificationService,
private stateProvider: StateProvider, private stateProvider: StateProvider,
private billingAccountProfileStateService: BillingAccountProfileStateService, private billingAccountProfileStateService: BillingAccountProfileStateService,
private platformUtilsService: PlatformUtilsService, private platformUtilsService: PlatformUtilsService,
private kdfConfigService: KdfConfigService, private kdfConfigService: KdfConfigService,
private syncService: SyncService, private syncService: SyncService,
private accountService: AccountService, private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
) { ) {}
this.pollUntilSynced();
this.premiumBannerState = this.stateProvider.getActive(PREMIUM_BANNER_REPROMPT_KEY);
this.sessionBannerState = this.stateProvider.getActive(BANNERS_DISMISSED_DISK_KEY);
const premiumSources$ = this.accountService.activeAccount$.pipe( shouldShowPremiumBanner$(userId: UserId): Observable<boolean> {
take(1), const premiumBannerState = this.premiumBannerState(userId);
switchMap((account) => { const premiumSources$ = combineLatest([
return combineLatest([ this.billingAccountProfileStateService.hasPremiumFromAnySource$(userId),
account premiumBannerState.state$,
? this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id) ]);
: of(false),
this.premiumBannerState.state$,
]);
}),
);
this.shouldShowPremiumBanner$ = this.syncCompleted$.pipe( return this.syncService.lastSync$(userId).pipe(
filter((lastSync) => lastSync !== null),
take(1), // Wait until the first sync is complete before considering the premium status take(1), // Wait until the first sync is complete before considering the premium status
mergeMap(() => premiumSources$), mergeMap(() => premiumSources$),
map(([canAccessPremium, dismissedState]) => { map(([canAccessPremium, dismissedState]) => {
@@ -122,9 +90,9 @@ export class VaultBannersService {
} }
/** Returns true when the update browser banner should be shown */ /** Returns true when the update browser banner should be shown */
async shouldShowUpdateBrowserBanner(): Promise<boolean> { async shouldShowUpdateBrowserBanner(userId: UserId): Promise<boolean> {
const outdatedBrowser = window.navigator.userAgent.indexOf("MSIE") !== -1; const outdatedBrowser = window.navigator.userAgent.indexOf("MSIE") !== -1;
const alreadyDismissed = (await this.getBannerDismissedState()).includes( const alreadyDismissed = (await this.getBannerDismissedState(userId)).includes(
VisibleVaultBanner.OutdatedBrowser, VisibleVaultBanner.OutdatedBrowser,
); );
@@ -132,10 +100,12 @@ export class VaultBannersService {
} }
/** Returns true when the verify email banner should be shown */ /** Returns true when the verify email banner should be shown */
async shouldShowVerifyEmailBanner(): Promise<boolean> { async shouldShowVerifyEmailBanner(userId: UserId): Promise<boolean> {
const needsVerification = !(await this.tokenService.getEmailVerified()); const needsVerification = !(
await firstValueFrom(this.accountService.accounts$.pipe(map((accounts) => accounts[userId])))
)?.emailVerified;
const alreadyDismissed = (await this.getBannerDismissedState()).includes( const alreadyDismissed = (await this.getBannerDismissedState(userId)).includes(
VisibleVaultBanner.VerifyEmail, VisibleVaultBanner.VerifyEmail,
); );
@@ -143,12 +113,14 @@ export class VaultBannersService {
} }
/** Returns true when the low KDF iteration banner should be shown */ /** Returns true when the low KDF iteration banner should be shown */
async shouldShowLowKDFBanner(): Promise<boolean> { async shouldShowLowKDFBanner(userId: UserId): Promise<boolean> {
const hasLowKDF = (await this.userVerificationService.hasMasterPassword()) const hasLowKDF = (
? await this.isLowKdfIteration() await firstValueFrom(this.userDecryptionOptionsService.userDecryptionOptionsById$(userId))
)?.hasMasterPassword
? await this.isLowKdfIteration(userId)
: false; : false;
const alreadyDismissed = (await this.getBannerDismissedState()).includes( const alreadyDismissed = (await this.getBannerDismissedState(userId)).includes(
VisibleVaultBanner.KDFSettings, VisibleVaultBanner.KDFSettings,
); );
@@ -156,11 +128,11 @@ export class VaultBannersService {
} }
/** Dismiss the given banner and perform any respective side effects */ /** Dismiss the given banner and perform any respective side effects */
async dismissBanner(banner: SessionBanners): Promise<void> { async dismissBanner(userId: UserId, banner: SessionBanners): Promise<void> {
if (banner === VisibleVaultBanner.Premium) { if (banner === VisibleVaultBanner.Premium) {
await this.dismissPremiumBanner(); await this.dismissPremiumBanner(userId);
} else { } else {
await this.sessionBannerState.update((current) => { await this.sessionBannerState(userId).update((current) => {
const bannersDismissed = current ?? []; const bannersDismissed = current ?? [];
return [...bannersDismissed, banner]; return [...bannersDismissed, banner];
@@ -168,16 +140,32 @@ export class VaultBannersService {
} }
} }
/**
*
* @returns a SingleUserState for the premium banner reprompt state
*/
private premiumBannerState(userId: UserId): SingleUserState<PremiumBannerReprompt> {
return this.stateProvider.getUser(userId, PREMIUM_BANNER_REPROMPT_KEY);
}
/**
*
* @returns a SingleUserState for the session banners dismissed state
*/
private sessionBannerState(userId: UserId): SingleUserState<SessionBanners[]> {
return this.stateProvider.getUser(userId, BANNERS_DISMISSED_DISK_KEY);
}
/** Returns banners that have already been dismissed */ /** Returns banners that have already been dismissed */
private async getBannerDismissedState(): Promise<SessionBanners[]> { private async getBannerDismissedState(userId: UserId): Promise<SessionBanners[]> {
// `state$` can emit null when a value has not been set yet, // `state$` can emit null when a value has not been set yet,
// use nullish coalescing to default to an empty array // use nullish coalescing to default to an empty array
return (await firstValueFrom(this.sessionBannerState.state$)) ?? []; return (await firstValueFrom(this.sessionBannerState(userId).state$)) ?? [];
} }
/** Increment dismissal state of the premium banner */ /** Increment dismissal state of the premium banner */
private async dismissPremiumBanner(): Promise<void> { private async dismissPremiumBanner(userId: UserId): Promise<void> {
await this.premiumBannerState.update((current) => { await this.premiumBannerState(userId).update((current) => {
const numberOfDismissals = current?.numberOfDismissals ?? 0; const numberOfDismissals = current?.numberOfDismissals ?? 0;
const now = new Date(); const now = new Date();
@@ -213,22 +201,11 @@ export class VaultBannersService {
}); });
} }
private async isLowKdfIteration() { private async isLowKdfIteration(userId: UserId) {
const kdfConfig = await this.kdfConfigService.getKdfConfig(); const kdfConfig = await firstValueFrom(this.kdfConfigService.getKdfConfig$(userId));
return ( return (
kdfConfig.kdfType === KdfType.PBKDF2_SHA256 && kdfConfig.kdfType === KdfType.PBKDF2_SHA256 &&
kdfConfig.iterations < PBKDF2KdfConfig.ITERATIONS.defaultValue kdfConfig.iterations < PBKDF2KdfConfig.ITERATIONS.defaultValue
); );
} }
/** Poll the `syncService` until a sync is completed */
private pollUntilSynced() {
const interval = setInterval(async () => {
const lastSync = await this.syncService.getLastSync();
if (lastSync !== null) {
clearInterval(interval);
this.syncCompleted$.next();
}
}, 200);
}
} }

View File

@@ -2,13 +2,17 @@ import { ComponentFixture, TestBed } from "@angular/core/testing";
import { By } from "@angular/platform-browser"; import { By } from "@angular/platform-browser";
import { RouterTestingModule } from "@angular/router/testing"; import { RouterTestingModule } from "@angular/router/testing";
import { mock } from "jest-mock-extended"; import { mock } from "jest-mock-extended";
import { BehaviorSubject } from "rxjs"; import { BehaviorSubject, Observable } from "rxjs";
import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec";
import { UserId } from "@bitwarden/common/types/guid";
import { BannerComponent, BannerModule } from "@bitwarden/components"; import { BannerComponent, BannerModule } from "@bitwarden/components";
import { VerifyEmailComponent } from "../../../auth/settings/verify-email.component"; import { VerifyEmailComponent } from "../../../auth/settings/verify-email.component";
@@ -21,21 +25,25 @@ describe("VaultBannersComponent", () => {
let component: VaultBannersComponent; let component: VaultBannersComponent;
let fixture: ComponentFixture<VaultBannersComponent>; let fixture: ComponentFixture<VaultBannersComponent>;
const premiumBanner$ = new BehaviorSubject<boolean>(false); const premiumBanner$ = new BehaviorSubject<boolean>(false);
const mockUserId = Utils.newGuid() as UserId;
const bannerService = mock<VaultBannersService>({ const bannerService = mock<VaultBannersService>({
shouldShowPremiumBanner$: premiumBanner$, shouldShowPremiumBanner$: jest.fn((userId$: Observable<UserId>) => premiumBanner$),
shouldShowUpdateBrowserBanner: jest.fn(), shouldShowUpdateBrowserBanner: jest.fn(),
shouldShowVerifyEmailBanner: jest.fn(), shouldShowVerifyEmailBanner: jest.fn(),
shouldShowLowKDFBanner: jest.fn(), shouldShowLowKDFBanner: jest.fn(),
dismissBanner: jest.fn(), dismissBanner: jest.fn(),
}); });
const accountService: FakeAccountService = mockAccountServiceWith(mockUserId);
beforeEach(async () => { beforeEach(async () => {
bannerService.shouldShowPremiumBanner$ = premiumBanner$;
bannerService.shouldShowUpdateBrowserBanner.mockResolvedValue(false); bannerService.shouldShowUpdateBrowserBanner.mockResolvedValue(false);
bannerService.shouldShowVerifyEmailBanner.mockResolvedValue(false); bannerService.shouldShowVerifyEmailBanner.mockResolvedValue(false);
bannerService.shouldShowLowKDFBanner.mockResolvedValue(false); bannerService.shouldShowLowKDFBanner.mockResolvedValue(false);
premiumBanner$.next(false);
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [ imports: [
BannerModule, BannerModule,
@@ -62,6 +70,10 @@ describe("VaultBannersComponent", () => {
provide: TokenService, provide: TokenService,
useValue: mock<TokenService>(), useValue: mock<TokenService>(),
}, },
{
provide: AccountService,
useValue: accountService,
},
], ],
}) })
.overrideProvider(VaultBannersService, { useValue: bannerService }) .overrideProvider(VaultBannersService, { useValue: bannerService })
@@ -135,7 +147,7 @@ describe("VaultBannersComponent", () => {
dismissButton.dispatchEvent(new Event("click")); dismissButton.dispatchEvent(new Event("click"));
expect(bannerService.dismissBanner).toHaveBeenCalledWith(banner); expect(bannerService.dismissBanner).toHaveBeenCalledWith(mockUserId, banner);
expect(component.visibleBanners).toEqual([]); expect(component.visibleBanners).toEqual([]);
}); });

View File

@@ -2,8 +2,9 @@
// @ts-strict-ignore // @ts-strict-ignore
import { Component, Input, OnInit } from "@angular/core"; import { Component, Input, OnInit } from "@angular/core";
import { Router } from "@angular/router"; import { Router } from "@angular/router";
import { Observable } from "rxjs"; import { firstValueFrom, map, Observable, switchMap } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { BannerModule } from "@bitwarden/components"; import { BannerModule } from "@bitwarden/components";
@@ -26,12 +27,17 @@ export class VaultBannersComponent implements OnInit {
VisibleVaultBanner = VisibleVaultBanner; VisibleVaultBanner = VisibleVaultBanner;
@Input() organizationsPaymentStatus: FreeTrial[] = []; @Input() organizationsPaymentStatus: FreeTrial[] = [];
private activeUserId$ = this.accountService.activeAccount$.pipe(map((a) => a?.id));
constructor( constructor(
private vaultBannerService: VaultBannersService, private vaultBannerService: VaultBannersService,
private router: Router, private router: Router,
private i18nService: I18nService, private i18nService: I18nService,
private accountService: AccountService,
) { ) {
this.premiumBannerVisible$ = this.vaultBannerService.shouldShowPremiumBanner$; this.premiumBannerVisible$ = this.activeUserId$.pipe(
switchMap((userId) => this.vaultBannerService.shouldShowPremiumBanner$(userId)),
);
} }
async ngOnInit(): Promise<void> { async ngOnInit(): Promise<void> {
@@ -39,7 +45,8 @@ export class VaultBannersComponent implements OnInit {
} }
async dismissBanner(banner: VisibleVaultBanner): Promise<void> { async dismissBanner(banner: VisibleVaultBanner): Promise<void> {
await this.vaultBannerService.dismissBanner(banner); const activeUserId = await firstValueFrom(this.activeUserId$);
await this.vaultBannerService.dismissBanner(activeUserId, banner);
await this.determineVisibleBanners(); await this.determineVisibleBanners();
} }
@@ -57,9 +64,12 @@ export class VaultBannersComponent implements OnInit {
/** Determine which banners should be present */ /** Determine which banners should be present */
private async determineVisibleBanners(): Promise<void> { private async determineVisibleBanners(): Promise<void> {
const showBrowserOutdated = await this.vaultBannerService.shouldShowUpdateBrowserBanner(); const activeUserId = await firstValueFrom(this.activeUserId$);
const showVerifyEmail = await this.vaultBannerService.shouldShowVerifyEmailBanner();
const showLowKdf = await this.vaultBannerService.shouldShowLowKDFBanner(); const showBrowserOutdated =
await this.vaultBannerService.shouldShowUpdateBrowserBanner(activeUserId);
const showVerifyEmail = await this.vaultBannerService.shouldShowVerifyEmailBanner(activeUserId);
const showLowKdf = await this.vaultBannerService.shouldShowLowKDFBanner(activeUserId);
this.visibleBanners = [ this.visibleBanners = [
showBrowserOutdated ? VisibleVaultBanner.OutdatedBrowser : null, showBrowserOutdated ? VisibleVaultBanner.OutdatedBrowser : null,