mirror of
https://github.com/bitwarden/browser.git
synced 2025-04-07 18:57:06 +02: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:
parent
06ca00f3c1
commit
14568f11dc
@ -1,11 +1,14 @@
|
||||
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 { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import {
|
||||
UserDecryptionOptions,
|
||||
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 { 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 { FakeStateProvider, mockAccountServiceWith } from "@bitwarden/common/spec";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
@ -22,18 +25,20 @@ describe("VaultBannersService", () => {
|
||||
let service: VaultBannersService;
|
||||
const isSelfHost = jest.fn().mockReturnValue(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 getEmailVerified = jest.fn().mockResolvedValue(true);
|
||||
const hasMasterPassword = jest.fn().mockResolvedValue(true);
|
||||
const getKdfConfig = jest
|
||||
.fn()
|
||||
.mockResolvedValue({ kdfType: KdfType.PBKDF2_SHA256, iterations: 600000 });
|
||||
const getLastSync = jest.fn().mockResolvedValue(null);
|
||||
const lastSync$ = new BehaviorSubject<Date | null>(null);
|
||||
const userDecryptionOptions$ = new BehaviorSubject<UserDecryptionOptions>({
|
||||
hasMasterPassword: true,
|
||||
});
|
||||
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(() => {
|
||||
jest.useFakeTimers();
|
||||
getLastSync.mockClear().mockResolvedValue(new Date("2024-05-14"));
|
||||
lastSync$.next(new Date("2024-05-14"));
|
||||
isSelfHost.mockClear();
|
||||
getEmailVerified.mockClear().mockResolvedValue(true);
|
||||
|
||||
@ -52,25 +57,27 @@ describe("VaultBannersService", () => {
|
||||
provide: StateProvider,
|
||||
useValue: fakeStateProvider,
|
||||
},
|
||||
{
|
||||
provide: PlatformUtilsService,
|
||||
useValue: { isSelfHost },
|
||||
},
|
||||
{
|
||||
provide: AccountService,
|
||||
useValue: mockAccountServiceWith(userId),
|
||||
},
|
||||
{
|
||||
provide: TokenService,
|
||||
useValue: { getEmailVerified },
|
||||
},
|
||||
{
|
||||
provide: UserVerificationService,
|
||||
useValue: { hasMasterPassword },
|
||||
useValue: { accounts$ },
|
||||
},
|
||||
{
|
||||
provide: KdfConfigService,
|
||||
useValue: { getKdfConfig },
|
||||
useValue: { getKdfConfig$: () => kdfConfig$ },
|
||||
},
|
||||
{
|
||||
provide: SyncService,
|
||||
useValue: { getLastSync },
|
||||
useValue: { lastSync$: () => lastSync$ },
|
||||
},
|
||||
{
|
||||
provide: UserDecryptionOptionsServiceAbstraction,
|
||||
useValue: {
|
||||
userDecryptionOptionsById$: () => userDecryptionOptions$,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
@ -82,39 +89,38 @@ describe("VaultBannersService", () => {
|
||||
|
||||
describe("Premium", () => {
|
||||
it("waits until sync is completed before showing premium banner", async () => {
|
||||
getLastSync.mockResolvedValue(new Date("2024-05-14"));
|
||||
hasPremiumFromAnySource$.next(false);
|
||||
isSelfHost.mockReturnValue(false);
|
||||
lastSync$.next(null);
|
||||
|
||||
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 () => {
|
||||
getLastSync.mockResolvedValue(new Date("2024-05-14"));
|
||||
hasPremiumFromAnySource$.next(false);
|
||||
isSelfHost.mockReturnValue(true);
|
||||
|
||||
service = TestBed.inject(VaultBannersService);
|
||||
|
||||
jest.advanceTimersByTime(201);
|
||||
|
||||
expect(await firstValueFrom(service.shouldShowPremiumBanner$)).toBe(false);
|
||||
expect(await firstValueFrom(service.shouldShowPremiumBanner$(userId))).toBe(false);
|
||||
});
|
||||
|
||||
it("does not show a premium banner when they have access to premium", async () => {
|
||||
getLastSync.mockResolvedValue(new Date("2024-05-14"));
|
||||
hasPremiumFromAnySource$.next(true);
|
||||
isSelfHost.mockReturnValue(false);
|
||||
|
||||
service = TestBed.inject(VaultBannersService);
|
||||
|
||||
jest.advanceTimersByTime(201);
|
||||
|
||||
expect(await firstValueFrom(service.shouldShowPremiumBanner$)).toBe(false);
|
||||
expect(await firstValueFrom(service.shouldShowPremiumBanner$(userId))).toBe(false);
|
||||
});
|
||||
|
||||
describe("dismissing", () => {
|
||||
@ -125,7 +131,7 @@ describe("VaultBannersService", () => {
|
||||
jest.setSystemTime(date.getTime());
|
||||
|
||||
service = TestBed.inject(VaultBannersService);
|
||||
await service.dismissBanner(VisibleVaultBanner.Premium);
|
||||
await service.dismissBanner(userId, VisibleVaultBanner.Premium);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@ -134,7 +140,7 @@ describe("VaultBannersService", () => {
|
||||
|
||||
it("updates state on first dismiss", async () => {
|
||||
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");
|
||||
@ -148,7 +154,7 @@ describe("VaultBannersService", () => {
|
||||
|
||||
it("updates state on second dismiss", async () => {
|
||||
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");
|
||||
@ -162,7 +168,7 @@ describe("VaultBannersService", () => {
|
||||
|
||||
it("updates state on third dismiss", async () => {
|
||||
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");
|
||||
@ -178,40 +184,40 @@ describe("VaultBannersService", () => {
|
||||
|
||||
describe("KDFSettings", () => {
|
||||
beforeEach(async () => {
|
||||
hasMasterPassword.mockResolvedValue(true);
|
||||
getKdfConfig.mockResolvedValue({ kdfType: KdfType.PBKDF2_SHA256, iterations: 599999 });
|
||||
userDecryptionOptions$.next({ hasMasterPassword: true });
|
||||
kdfConfig$.next({ kdfType: KdfType.PBKDF2_SHA256, iterations: 599999 });
|
||||
});
|
||||
|
||||
it("shows low KDF iteration banner", async () => {
|
||||
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 () => {
|
||||
getKdfConfig.mockResolvedValue({ kdfType: KdfType.Argon2id, iterations: 600001 });
|
||||
kdfConfig$.next({ kdfType: KdfType.Argon2id, iterations: 600001 });
|
||||
|
||||
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 () => {
|
||||
getKdfConfig.mockResolvedValue({ kdfType: KdfType.PBKDF2_SHA256, iterations: 600001 });
|
||||
kdfConfig$.next({ kdfType: KdfType.PBKDF2_SHA256, iterations: 600001 });
|
||||
|
||||
service = TestBed.inject(VaultBannersService);
|
||||
|
||||
expect(await service.shouldShowLowKDFBanner()).toBe(false);
|
||||
expect(await service.shouldShowLowKDFBanner(userId)).toBe(false);
|
||||
});
|
||||
|
||||
it("dismisses low KDF iteration banner", async () => {
|
||||
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 () => {
|
||||
service = TestBed.inject(VaultBannersService);
|
||||
|
||||
expect(await service.shouldShowUpdateBrowserBanner()).toBe(true);
|
||||
expect(await service.shouldShowUpdateBrowserBanner(userId)).toBe(true);
|
||||
});
|
||||
|
||||
it("dismisses outdated browser banner", async () => {
|
||||
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", () => {
|
||||
beforeEach(async () => {
|
||||
getEmailVerified.mockResolvedValue(false);
|
||||
accounts$.next({
|
||||
[userId]: {
|
||||
...accounts$.value[userId],
|
||||
emailVerified: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("shows verify email banner", async () => {
|
||||
service = TestBed.inject(VaultBannersService);
|
||||
|
||||
expect(await service.shouldShowVerifyEmailBanner()).toBe(true);
|
||||
expect(await service.shouldShowVerifyEmailBanner(userId)).toBe(true);
|
||||
});
|
||||
|
||||
it("dismisses verify email banner", async () => {
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,28 +1,18 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
import {
|
||||
Subject,
|
||||
Observable,
|
||||
combineLatest,
|
||||
firstValueFrom,
|
||||
map,
|
||||
mergeMap,
|
||||
take,
|
||||
switchMap,
|
||||
of,
|
||||
} from "rxjs";
|
||||
import { Observable, combineLatest, firstValueFrom, map, filter, mergeMap, take } from "rxjs";
|
||||
|
||||
import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
|
||||
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 { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import {
|
||||
StateProvider,
|
||||
ActiveUserState,
|
||||
PREMIUM_BANNER_DISK_LOCAL,
|
||||
BANNERS_DISMISSED_DISK,
|
||||
UserKeyDefinition,
|
||||
SingleUserState,
|
||||
} from "@bitwarden/common/platform/state";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
import { PBKDF2KdfConfig, KdfConfigService, KdfType } from "@bitwarden/key-management";
|
||||
|
||||
@ -62,47 +52,25 @@ export const BANNERS_DISMISSED_DISK_KEY = new UserKeyDefinition<SessionBanners[]
|
||||
|
||||
@Injectable()
|
||||
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(
|
||||
private tokenService: TokenService,
|
||||
private userVerificationService: UserVerificationService,
|
||||
private accountService: AccountService,
|
||||
private stateProvider: StateProvider,
|
||||
private billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private kdfConfigService: KdfConfigService,
|
||||
private syncService: SyncService,
|
||||
private accountService: AccountService,
|
||||
) {
|
||||
this.pollUntilSynced();
|
||||
this.premiumBannerState = this.stateProvider.getActive(PREMIUM_BANNER_REPROMPT_KEY);
|
||||
this.sessionBannerState = this.stateProvider.getActive(BANNERS_DISMISSED_DISK_KEY);
|
||||
private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
|
||||
) {}
|
||||
|
||||
const premiumSources$ = this.accountService.activeAccount$.pipe(
|
||||
take(1),
|
||||
switchMap((account) => {
|
||||
return combineLatest([
|
||||
account
|
||||
? this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id)
|
||||
: of(false),
|
||||
this.premiumBannerState.state$,
|
||||
]);
|
||||
}),
|
||||
);
|
||||
shouldShowPremiumBanner$(userId: UserId): Observable<boolean> {
|
||||
const premiumBannerState = this.premiumBannerState(userId);
|
||||
const premiumSources$ = combineLatest([
|
||||
this.billingAccountProfileStateService.hasPremiumFromAnySource$(userId),
|
||||
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
|
||||
mergeMap(() => premiumSources$),
|
||||
map(([canAccessPremium, dismissedState]) => {
|
||||
@ -122,9 +90,9 @@ export class VaultBannersService {
|
||||
}
|
||||
|
||||
/** 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 alreadyDismissed = (await this.getBannerDismissedState()).includes(
|
||||
const alreadyDismissed = (await this.getBannerDismissedState(userId)).includes(
|
||||
VisibleVaultBanner.OutdatedBrowser,
|
||||
);
|
||||
|
||||
@ -132,10 +100,12 @@ export class VaultBannersService {
|
||||
}
|
||||
|
||||
/** Returns true when the verify email banner should be shown */
|
||||
async shouldShowVerifyEmailBanner(): Promise<boolean> {
|
||||
const needsVerification = !(await this.tokenService.getEmailVerified());
|
||||
async shouldShowVerifyEmailBanner(userId: UserId): Promise<boolean> {
|
||||
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,
|
||||
);
|
||||
|
||||
@ -143,12 +113,14 @@ export class VaultBannersService {
|
||||
}
|
||||
|
||||
/** Returns true when the low KDF iteration banner should be shown */
|
||||
async shouldShowLowKDFBanner(): Promise<boolean> {
|
||||
const hasLowKDF = (await this.userVerificationService.hasMasterPassword())
|
||||
? await this.isLowKdfIteration()
|
||||
async shouldShowLowKDFBanner(userId: UserId): Promise<boolean> {
|
||||
const hasLowKDF = (
|
||||
await firstValueFrom(this.userDecryptionOptionsService.userDecryptionOptionsById$(userId))
|
||||
)?.hasMasterPassword
|
||||
? await this.isLowKdfIteration(userId)
|
||||
: false;
|
||||
|
||||
const alreadyDismissed = (await this.getBannerDismissedState()).includes(
|
||||
const alreadyDismissed = (await this.getBannerDismissedState(userId)).includes(
|
||||
VisibleVaultBanner.KDFSettings,
|
||||
);
|
||||
|
||||
@ -156,11 +128,11 @@ export class VaultBannersService {
|
||||
}
|
||||
|
||||
/** 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) {
|
||||
await this.dismissPremiumBanner();
|
||||
await this.dismissPremiumBanner(userId);
|
||||
} else {
|
||||
await this.sessionBannerState.update((current) => {
|
||||
await this.sessionBannerState(userId).update((current) => {
|
||||
const bannersDismissed = current ?? [];
|
||||
|
||||
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 */
|
||||
private async getBannerDismissedState(): Promise<SessionBanners[]> {
|
||||
private async getBannerDismissedState(userId: UserId): Promise<SessionBanners[]> {
|
||||
// `state$` can emit null when a value has not been set yet,
|
||||
// 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 */
|
||||
private async dismissPremiumBanner(): Promise<void> {
|
||||
await this.premiumBannerState.update((current) => {
|
||||
private async dismissPremiumBanner(userId: UserId): Promise<void> {
|
||||
await this.premiumBannerState(userId).update((current) => {
|
||||
const numberOfDismissals = current?.numberOfDismissals ?? 0;
|
||||
const now = new Date();
|
||||
|
||||
@ -213,22 +201,11 @@ export class VaultBannersService {
|
||||
});
|
||||
}
|
||||
|
||||
private async isLowKdfIteration() {
|
||||
const kdfConfig = await this.kdfConfigService.getKdfConfig();
|
||||
private async isLowKdfIteration(userId: UserId) {
|
||||
const kdfConfig = await firstValueFrom(this.kdfConfigService.getKdfConfig$(userId));
|
||||
return (
|
||||
kdfConfig.kdfType === KdfType.PBKDF2_SHA256 &&
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -2,13 +2,17 @@ import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { By } from "@angular/platform-browser";
|
||||
import { RouterTestingModule } from "@angular/router/testing";
|
||||
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 { 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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.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 { VerifyEmailComponent } from "../../../auth/settings/verify-email.component";
|
||||
@ -21,21 +25,25 @@ describe("VaultBannersComponent", () => {
|
||||
let component: VaultBannersComponent;
|
||||
let fixture: ComponentFixture<VaultBannersComponent>;
|
||||
const premiumBanner$ = new BehaviorSubject<boolean>(false);
|
||||
const mockUserId = Utils.newGuid() as UserId;
|
||||
|
||||
const bannerService = mock<VaultBannersService>({
|
||||
shouldShowPremiumBanner$: premiumBanner$,
|
||||
shouldShowPremiumBanner$: jest.fn((userId$: Observable<UserId>) => premiumBanner$),
|
||||
shouldShowUpdateBrowserBanner: jest.fn(),
|
||||
shouldShowVerifyEmailBanner: jest.fn(),
|
||||
shouldShowLowKDFBanner: jest.fn(),
|
||||
dismissBanner: jest.fn(),
|
||||
});
|
||||
|
||||
const accountService: FakeAccountService = mockAccountServiceWith(mockUserId);
|
||||
|
||||
beforeEach(async () => {
|
||||
bannerService.shouldShowPremiumBanner$ = premiumBanner$;
|
||||
bannerService.shouldShowUpdateBrowserBanner.mockResolvedValue(false);
|
||||
bannerService.shouldShowVerifyEmailBanner.mockResolvedValue(false);
|
||||
bannerService.shouldShowLowKDFBanner.mockResolvedValue(false);
|
||||
|
||||
premiumBanner$.next(false);
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [
|
||||
BannerModule,
|
||||
@ -62,6 +70,10 @@ describe("VaultBannersComponent", () => {
|
||||
provide: TokenService,
|
||||
useValue: mock<TokenService>(),
|
||||
},
|
||||
{
|
||||
provide: AccountService,
|
||||
useValue: accountService,
|
||||
},
|
||||
],
|
||||
})
|
||||
.overrideProvider(VaultBannersService, { useValue: bannerService })
|
||||
@ -135,7 +147,7 @@ describe("VaultBannersComponent", () => {
|
||||
|
||||
dismissButton.dispatchEvent(new Event("click"));
|
||||
|
||||
expect(bannerService.dismissBanner).toHaveBeenCalledWith(banner);
|
||||
expect(bannerService.dismissBanner).toHaveBeenCalledWith(mockUserId, banner);
|
||||
|
||||
expect(component.visibleBanners).toEqual([]);
|
||||
});
|
||||
|
@ -2,8 +2,9 @@
|
||||
// @ts-strict-ignore
|
||||
import { Component, Input, OnInit } from "@angular/core";
|
||||
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 { BannerModule } from "@bitwarden/components";
|
||||
|
||||
@ -26,12 +27,17 @@ export class VaultBannersComponent implements OnInit {
|
||||
VisibleVaultBanner = VisibleVaultBanner;
|
||||
@Input() organizationsPaymentStatus: FreeTrial[] = [];
|
||||
|
||||
private activeUserId$ = this.accountService.activeAccount$.pipe(map((a) => a?.id));
|
||||
|
||||
constructor(
|
||||
private vaultBannerService: VaultBannersService,
|
||||
private router: Router,
|
||||
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> {
|
||||
@ -39,7 +45,8 @@ export class VaultBannersComponent implements OnInit {
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
@ -57,9 +64,12 @@ export class VaultBannersComponent implements OnInit {
|
||||
|
||||
/** Determine which banners should be present */
|
||||
private async determineVisibleBanners(): Promise<void> {
|
||||
const showBrowserOutdated = await this.vaultBannerService.shouldShowUpdateBrowserBanner();
|
||||
const showVerifyEmail = await this.vaultBannerService.shouldShowVerifyEmailBanner();
|
||||
const showLowKdf = await this.vaultBannerService.shouldShowLowKDFBanner();
|
||||
const activeUserId = await firstValueFrom(this.activeUserId$);
|
||||
|
||||
const showBrowserOutdated =
|
||||
await this.vaultBannerService.shouldShowUpdateBrowserBanner(activeUserId);
|
||||
const showVerifyEmail = await this.vaultBannerService.shouldShowVerifyEmailBanner(activeUserId);
|
||||
const showLowKdf = await this.vaultBannerService.shouldShowLowKDFBanner(activeUserId);
|
||||
|
||||
this.visibleBanners = [
|
||||
showBrowserOutdated ? VisibleVaultBanner.OutdatedBrowser : null,
|
||||
|
Loading…
Reference in New Issue
Block a user