mirror of
https://github.com/bitwarden/browser
synced 2026-02-04 10:43:47 +00:00
Remove premium banner from vault page
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { TestBed } from "@angular/core/testing";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { BehaviorSubject, firstValueFrom, of, take, timeout } from "rxjs";
|
||||
import { BehaviorSubject, of } from "rxjs";
|
||||
|
||||
import {
|
||||
AuthRequestServiceAbstraction,
|
||||
@@ -22,11 +22,7 @@ import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
import { KdfConfigService, KdfType } from "@bitwarden/key-management";
|
||||
|
||||
import {
|
||||
PREMIUM_BANNER_REPROMPT_KEY,
|
||||
VaultBannersService,
|
||||
VisibleVaultBanner,
|
||||
} from "./vault-banners.service";
|
||||
import { VaultBannersService, VisibleVaultBanner } from "./vault-banners.service";
|
||||
|
||||
describe("VaultBannersService", () => {
|
||||
let service: VaultBannersService;
|
||||
@@ -111,101 +107,6 @@ describe("VaultBannersService", () => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
describe("Premium", () => {
|
||||
it("waits until sync is completed before showing premium banner", async () => {
|
||||
hasPremiumFromAnySource$.next(false);
|
||||
isSelfHost.mockReturnValue(false);
|
||||
lastSync$.next(null);
|
||||
|
||||
service = TestBed.inject(VaultBannersService);
|
||||
|
||||
const premiumBanner$ = service.shouldShowPremiumBanner$(userId);
|
||||
|
||||
// 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 () => {
|
||||
hasPremiumFromAnySource$.next(false);
|
||||
isSelfHost.mockReturnValue(true);
|
||||
|
||||
service = TestBed.inject(VaultBannersService);
|
||||
|
||||
expect(await firstValueFrom(service.shouldShowPremiumBanner$(userId))).toBe(false);
|
||||
});
|
||||
|
||||
it("does not show a premium banner when they have access to premium", async () => {
|
||||
hasPremiumFromAnySource$.next(true);
|
||||
isSelfHost.mockReturnValue(false);
|
||||
|
||||
service = TestBed.inject(VaultBannersService);
|
||||
|
||||
expect(await firstValueFrom(service.shouldShowPremiumBanner$(userId))).toBe(false);
|
||||
});
|
||||
|
||||
describe("dismissing", () => {
|
||||
beforeEach(async () => {
|
||||
jest.useFakeTimers();
|
||||
const date = new Date("2023-06-08");
|
||||
date.setHours(0, 0, 0, 0);
|
||||
jest.setSystemTime(date.getTime());
|
||||
|
||||
service = TestBed.inject(VaultBannersService);
|
||||
await service.dismissBanner(userId, VisibleVaultBanner.Premium);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it("updates state on first dismiss", async () => {
|
||||
const state = await firstValueFrom(
|
||||
fakeStateProvider.getUser(userId, PREMIUM_BANNER_REPROMPT_KEY).state$,
|
||||
);
|
||||
|
||||
const oneWeekLater = new Date("2023-06-15");
|
||||
oneWeekLater.setHours(0, 0, 0, 0);
|
||||
|
||||
expect(state).toEqual({
|
||||
numberOfDismissals: 1,
|
||||
nextPromptDate: oneWeekLater.getTime(),
|
||||
});
|
||||
});
|
||||
|
||||
it("updates state on second dismiss", async () => {
|
||||
const state = await firstValueFrom(
|
||||
fakeStateProvider.getUser(userId, PREMIUM_BANNER_REPROMPT_KEY).state$,
|
||||
);
|
||||
|
||||
const oneMonthLater = new Date("2023-07-08");
|
||||
oneMonthLater.setHours(0, 0, 0, 0);
|
||||
|
||||
expect(state).toEqual({
|
||||
numberOfDismissals: 2,
|
||||
nextPromptDate: oneMonthLater.getTime(),
|
||||
});
|
||||
});
|
||||
|
||||
it("updates state on third dismiss", async () => {
|
||||
const state = await firstValueFrom(
|
||||
fakeStateProvider.getUser(userId, PREMIUM_BANNER_REPROMPT_KEY).state$,
|
||||
);
|
||||
|
||||
const oneYearLater = new Date("2024-06-08");
|
||||
oneYearLater.setHours(0, 0, 0, 0);
|
||||
|
||||
expect(state).toEqual({
|
||||
numberOfDismissals: 3,
|
||||
nextPromptDate: oneYearLater.getTime(),
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("KDFSettings", () => {
|
||||
beforeEach(async () => {
|
||||
userDecryptionOptions$.next({ hasMasterPassword: true });
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
import { Observable, combineLatest, firstValueFrom, map, filter, mergeMap, take } from "rxjs";
|
||||
import { firstValueFrom, map } from "rxjs";
|
||||
|
||||
import {
|
||||
AuthRequestServiceAbstraction,
|
||||
@@ -13,7 +13,6 @@ import { ConfigService } from "@bitwarden/common/platform/abstractions/config/co
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import {
|
||||
StateProvider,
|
||||
PREMIUM_BANNER_DISK_LOCAL,
|
||||
BANNERS_DISMISSED_DISK,
|
||||
UserKeyDefinition,
|
||||
SingleUserState,
|
||||
@@ -26,30 +25,14 @@ import { PBKDF2KdfConfig, KdfConfigService, KdfType } from "@bitwarden/key-manag
|
||||
export const VisibleVaultBanner = {
|
||||
KDFSettings: "kdf-settings",
|
||||
OutdatedBrowser: "outdated-browser",
|
||||
Premium: "premium",
|
||||
VerifyEmail: "verify-email",
|
||||
PendingAuthRequest: "pending-auth-request",
|
||||
} as const;
|
||||
|
||||
export type VisibleVaultBanner = UnionOfValues<typeof VisibleVaultBanner>;
|
||||
|
||||
type PremiumBannerReprompt = {
|
||||
numberOfDismissals: number;
|
||||
/** Timestamp representing when to show the prompt next */
|
||||
nextPromptDate: number;
|
||||
};
|
||||
|
||||
/** Banners that will be re-shown on a new session */
|
||||
type SessionBanners = Omit<VisibleVaultBanner, typeof VisibleVaultBanner.Premium>;
|
||||
|
||||
export const PREMIUM_BANNER_REPROMPT_KEY = new UserKeyDefinition<PremiumBannerReprompt>(
|
||||
PREMIUM_BANNER_DISK_LOCAL,
|
||||
"bannerReprompt",
|
||||
{
|
||||
deserializer: (bannerReprompt) => bannerReprompt,
|
||||
clearOn: [], // Do not clear user tutorials
|
||||
},
|
||||
);
|
||||
type SessionBanners = VisibleVaultBanner;
|
||||
|
||||
export const BANNERS_DISMISSED_DISK_KEY = new UserKeyDefinition<SessionBanners[]>(
|
||||
BANNERS_DISMISSED_DISK,
|
||||
@@ -100,33 +83,6 @@ export class VaultBannersService {
|
||||
}
|
||||
}
|
||||
|
||||
shouldShowPremiumBanner$(userId: UserId): Observable<boolean> {
|
||||
const premiumBannerState = this.premiumBannerState(userId);
|
||||
const premiumSources$ = combineLatest([
|
||||
this.billingAccountProfileStateService.hasPremiumFromAnySource$(userId),
|
||||
premiumBannerState.state$,
|
||||
]);
|
||||
|
||||
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]) => {
|
||||
const shouldShowPremiumBanner =
|
||||
!canAccessPremium && !this.platformUtilsService.isSelfHost();
|
||||
|
||||
// Check if nextPromptDate is in the past passed
|
||||
if (shouldShowPremiumBanner && dismissedState?.nextPromptDate) {
|
||||
const nextPromptDate = new Date(dismissedState.nextPromptDate);
|
||||
const now = new Date();
|
||||
return now >= nextPromptDate;
|
||||
}
|
||||
|
||||
return shouldShowPremiumBanner;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/** Returns true when the update browser banner should be shown */
|
||||
async shouldShowUpdateBrowserBanner(userId: UserId): Promise<boolean> {
|
||||
const outdatedBrowser = window.navigator.userAgent.indexOf("MSIE") !== -1;
|
||||
@@ -167,23 +123,11 @@ export class VaultBannersService {
|
||||
|
||||
/** Dismiss the given banner and perform any respective side effects */
|
||||
async dismissBanner(userId: UserId, banner: SessionBanners): Promise<void> {
|
||||
if (banner === VisibleVaultBanner.Premium) {
|
||||
await this.dismissPremiumBanner(userId);
|
||||
} else {
|
||||
await this.sessionBannerState(userId).update((current) => {
|
||||
const bannersDismissed = current ?? [];
|
||||
await this.sessionBannerState(userId).update((current) => {
|
||||
const bannersDismissed = current ?? [];
|
||||
|
||||
return [...bannersDismissed, banner];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns a SingleUserState for the premium banner reprompt state
|
||||
*/
|
||||
private premiumBannerState(userId: UserId): SingleUserState<PremiumBannerReprompt> {
|
||||
return this.stateProvider.getUser(userId, PREMIUM_BANNER_REPROMPT_KEY);
|
||||
return [...bannersDismissed, banner];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -201,44 +145,6 @@ export class VaultBannersService {
|
||||
return (await firstValueFrom(this.sessionBannerState(userId).state$)) ?? [];
|
||||
}
|
||||
|
||||
/** Increment dismissal state of the premium banner */
|
||||
private async dismissPremiumBanner(userId: UserId): Promise<void> {
|
||||
await this.premiumBannerState(userId).update((current) => {
|
||||
const numberOfDismissals = current?.numberOfDismissals ?? 0;
|
||||
const now = new Date();
|
||||
|
||||
// Set midnight of the current day
|
||||
now.setHours(0, 0, 0, 0);
|
||||
|
||||
// First dismissal, re-prompt in 1 week
|
||||
if (numberOfDismissals === 0) {
|
||||
now.setDate(now.getDate() + 7);
|
||||
return {
|
||||
numberOfDismissals: 1,
|
||||
nextPromptDate: now.getTime(),
|
||||
};
|
||||
}
|
||||
|
||||
// Second dismissal, re-prompt in 1 month
|
||||
if (numberOfDismissals === 1) {
|
||||
now.setMonth(now.getMonth() + 1);
|
||||
return {
|
||||
numberOfDismissals: 2,
|
||||
nextPromptDate: now.getTime(),
|
||||
};
|
||||
}
|
||||
|
||||
// 3+ dismissals, re-prompt each year
|
||||
// Avoid day/month edge cases and only increment year
|
||||
const nextYear = new Date(now.getFullYear() + 1, now.getMonth(), now.getDate());
|
||||
nextYear.setHours(0, 0, 0, 0);
|
||||
return {
|
||||
numberOfDismissals: numberOfDismissals + 1,
|
||||
nextPromptDate: nextYear.getTime(),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private async isLowKdfIteration(userId: UserId) {
|
||||
const kdfConfig = await firstValueFrom(this.kdfConfigService.getKdfConfig$(userId));
|
||||
return (
|
||||
|
||||
@@ -56,15 +56,3 @@
|
||||
(onDismiss)="dismissBanner(VisibleVaultBanner.VerifyEmail)"
|
||||
(onVerified)="dismissBanner(VisibleVaultBanner.VerifyEmail)"
|
||||
></app-verify-email>
|
||||
|
||||
<bit-banner
|
||||
id="premium-banner"
|
||||
bannerType="premium"
|
||||
*ngIf="premiumBannerVisible$ | async"
|
||||
(onClose)="dismissBanner(VisibleVaultBanner.Premium)"
|
||||
>
|
||||
{{ "premiumUpgradeUnlockFeatures" | i18n }}
|
||||
<a bitLink linkType="secondary" routerLink="/settings/subscription/premium">
|
||||
{{ "goPremium" | i18n }}
|
||||
</a>
|
||||
</bit-banner>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
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, Subject } from "rxjs";
|
||||
@@ -15,7 +14,7 @@ import { MessageListener } from "@bitwarden/common/platform/messaging";
|
||||
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 { BannerModule } from "@bitwarden/components";
|
||||
|
||||
import { VerifyEmailComponent } from "../../../auth/settings/verify-email.component";
|
||||
import { SharedModule } from "../../../shared";
|
||||
@@ -27,12 +26,10 @@ describe("VaultBannersComponent", () => {
|
||||
let component: VaultBannersComponent;
|
||||
let fixture: ComponentFixture<VaultBannersComponent>;
|
||||
let messageSubject: Subject<{ command: string }>;
|
||||
const premiumBanner$ = new BehaviorSubject<boolean>(false);
|
||||
const pendingAuthRequest$ = new BehaviorSubject<boolean>(false);
|
||||
const mockUserId = Utils.newGuid() as UserId;
|
||||
|
||||
const bannerService = mock<VaultBannersService>({
|
||||
shouldShowPremiumBanner$: jest.fn((userId: UserId) => premiumBanner$),
|
||||
shouldShowUpdateBrowserBanner: jest.fn(),
|
||||
shouldShowVerifyEmailBanner: jest.fn(),
|
||||
shouldShowLowKDFBanner: jest.fn(),
|
||||
@@ -50,7 +47,6 @@ describe("VaultBannersComponent", () => {
|
||||
bannerService.shouldShowVerifyEmailBanner.mockResolvedValue(false);
|
||||
bannerService.shouldShowLowKDFBanner.mockResolvedValue(false);
|
||||
pendingAuthRequest$.next(false);
|
||||
premiumBanner$.next(false);
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [
|
||||
@@ -105,26 +101,6 @@ describe("VaultBannersComponent", () => {
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
describe("premiumBannerVisible$", () => {
|
||||
it("shows premium banner", async () => {
|
||||
premiumBanner$.next(true);
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
const banner = fixture.debugElement.query(By.directive(BannerComponent));
|
||||
expect(banner.componentInstance.bannerType()).toBe("premium");
|
||||
});
|
||||
|
||||
it("dismisses premium banner", async () => {
|
||||
premiumBanner$.next(false);
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
const banner = fixture.debugElement.query(By.directive(BannerComponent));
|
||||
expect(banner).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("determineVisibleBanner", () => {
|
||||
[
|
||||
{
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import { Component, Input, OnInit } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { Router } from "@angular/router";
|
||||
import { filter, firstValueFrom, map, Observable, switchMap } from "rxjs";
|
||||
import { filter, firstValueFrom, map } from "rxjs";
|
||||
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { MessageListener } from "@bitwarden/common/platform/messaging";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { BannerModule } from "@bitwarden/components";
|
||||
import { OrganizationFreeTrialWarningComponent } from "@bitwarden/web-vault/app/billing/organizations/warnings/components";
|
||||
|
||||
@@ -30,7 +29,6 @@ import { VaultBannersService, VisibleVaultBanner } from "./services/vault-banner
|
||||
})
|
||||
export class VaultBannersComponent implements OnInit {
|
||||
visibleBanners: VisibleVaultBanner[] = [];
|
||||
premiumBannerVisible$: Observable<boolean>;
|
||||
VisibleVaultBanner = VisibleVaultBanner;
|
||||
@Input() organizations: Organization[] = [];
|
||||
|
||||
@@ -43,11 +41,6 @@ export class VaultBannersComponent implements OnInit {
|
||||
private messageListener: MessageListener,
|
||||
private configService: ConfigService,
|
||||
) {
|
||||
this.premiumBannerVisible$ = this.activeUserId$.pipe(
|
||||
filter((userId): userId is UserId => userId != null),
|
||||
switchMap((userId) => this.vaultBannerService.shouldShowPremiumBanner$(userId)),
|
||||
);
|
||||
|
||||
// Listen for auth request messages and show banner immediately
|
||||
this.messageListener.allMessages$
|
||||
.pipe(
|
||||
|
||||
Reference in New Issue
Block a user