mirror of
https://github.com/bitwarden/browser
synced 2026-01-21 11:53:34 +00:00
[PM-29061] Remove Feature Flag 24996 (#18009)
* refactor(billing): remove FF from vault banner * refactor(billing): remove from prompt service * chore(billing): remove feature flag * fix(billing): remove premium banner * tests(billing): remove premium banner tests * chore(vault): clean up premium banner dependencies * fix(billing): revert formatting * fix(billing): revert formatting * fix(billing): remove old FF * fix(billling): revert formatting
This commit is contained in:
@@ -5,8 +5,6 @@ import { VaultProfileService } from "@bitwarden/angular/vault/services/vault-pro
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { AccountService, Account } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { SyncService } from "@bitwarden/common/platform/sync/sync.service";
|
||||
@@ -26,7 +24,6 @@ import {
|
||||
describe("UnifiedUpgradePromptService", () => {
|
||||
let sut: UnifiedUpgradePromptService;
|
||||
const mockAccountService = mock<AccountService>();
|
||||
const mockConfigService = mock<ConfigService>();
|
||||
const mockBillingService = mock<BillingAccountProfileStateService>();
|
||||
const mockVaultProfileService = mock<VaultProfileService>();
|
||||
const mockSyncService = mock<SyncService>();
|
||||
@@ -59,7 +56,6 @@ describe("UnifiedUpgradePromptService", () => {
|
||||
function setupTestService() {
|
||||
sut = new UnifiedUpgradePromptService(
|
||||
mockAccountService,
|
||||
mockConfigService,
|
||||
mockBillingService,
|
||||
mockVaultProfileService,
|
||||
mockSyncService,
|
||||
@@ -80,7 +76,6 @@ describe("UnifiedUpgradePromptService", () => {
|
||||
beforeEach(() => {
|
||||
mockAccountService.activeAccount$ = accountSubject.asObservable();
|
||||
mockPlatformUtilsService.isSelfHost.mockReturnValue(false);
|
||||
mockConfigService.getFeatureFlag$.mockReturnValue(of(true));
|
||||
mockStateProvider.getUserState$.mockReturnValue(of(false));
|
||||
|
||||
setupTestService();
|
||||
@@ -96,7 +91,6 @@ describe("UnifiedUpgradePromptService", () => {
|
||||
mockAccountService.activeAccount$ = accountSubject.asObservable();
|
||||
mockDialogOpen.mockReset();
|
||||
mockReset(mockDialogService);
|
||||
mockReset(mockConfigService);
|
||||
mockReset(mockBillingService);
|
||||
mockReset(mockVaultProfileService);
|
||||
mockReset(mockSyncService);
|
||||
@@ -112,11 +106,10 @@ describe("UnifiedUpgradePromptService", () => {
|
||||
mockStateProvider.getUserState$.mockReturnValue(of(false));
|
||||
mockStateProvider.setUserState.mockResolvedValue(undefined);
|
||||
});
|
||||
it("should subscribe to account and feature flag observables when checking display conditions", async () => {
|
||||
it("should subscribe to account observables when checking display conditions", async () => {
|
||||
// Arrange
|
||||
mockPlatformUtilsService.isSelfHost.mockReturnValue(false);
|
||||
mockOrganizationService.memberOrganizations$.mockReturnValue(of([]));
|
||||
mockConfigService.getFeatureFlag$.mockReturnValue(of(false));
|
||||
mockBillingService.hasPremiumFromAnySource$.mockReturnValue(of(false));
|
||||
|
||||
setupTestService();
|
||||
@@ -125,34 +118,12 @@ describe("UnifiedUpgradePromptService", () => {
|
||||
await sut.displayUpgradePromptConditionally();
|
||||
|
||||
// Assert
|
||||
expect(mockConfigService.getFeatureFlag$).toHaveBeenCalledWith(
|
||||
FeatureFlag.PM24996_ImplementUpgradeFromFreeDialog,
|
||||
);
|
||||
expect(mockAccountService.activeAccount$).toBeDefined();
|
||||
});
|
||||
it("should not show dialog when feature flag is disabled", async () => {
|
||||
// Arrange
|
||||
mockPlatformUtilsService.isSelfHost.mockReturnValue(false);
|
||||
mockOrganizationService.memberOrganizations$.mockReturnValue(of([]));
|
||||
mockConfigService.getFeatureFlag$.mockReturnValue(of(false));
|
||||
mockBillingService.hasPremiumFromAnySource$.mockReturnValue(of(false));
|
||||
const recentDate = new Date();
|
||||
recentDate.setMinutes(recentDate.getMinutes() - 3); // 3 minutes old
|
||||
mockVaultProfileService.getProfileCreationDate.mockResolvedValue(recentDate);
|
||||
|
||||
setupTestService();
|
||||
// Act
|
||||
const result = await sut.displayUpgradePromptConditionally();
|
||||
|
||||
// Assert
|
||||
expect(result).toBeNull();
|
||||
expect(mockDialogOpen).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should not show dialog when user has premium", async () => {
|
||||
// Arrange
|
||||
mockPlatformUtilsService.isSelfHost.mockReturnValue(false);
|
||||
mockConfigService.getFeatureFlag$.mockReturnValue(of(true));
|
||||
mockBillingService.hasPremiumFromAnySource$.mockReturnValue(of(true));
|
||||
mockOrganizationService.memberOrganizations$.mockReturnValue(of([]));
|
||||
setupTestService();
|
||||
@@ -167,7 +138,6 @@ describe("UnifiedUpgradePromptService", () => {
|
||||
|
||||
it("should not show dialog when user has any organization membership", async () => {
|
||||
// Arrange
|
||||
mockConfigService.getFeatureFlag$.mockReturnValue(of(true));
|
||||
mockBillingService.hasPremiumFromAnySource$.mockReturnValue(of(false));
|
||||
mockOrganizationService.memberOrganizations$.mockReturnValue(of([{ id: "org1" } as any]));
|
||||
mockPlatformUtilsService.isSelfHost.mockReturnValue(false);
|
||||
@@ -183,7 +153,6 @@ describe("UnifiedUpgradePromptService", () => {
|
||||
|
||||
it("should not show dialog when profile is older than 5 minutes", async () => {
|
||||
// Arrange
|
||||
mockConfigService.getFeatureFlag$.mockReturnValue(of(true));
|
||||
mockBillingService.hasPremiumFromAnySource$.mockReturnValue(of(false));
|
||||
mockOrganizationService.memberOrganizations$.mockReturnValue(of([]));
|
||||
const oldDate = new Date();
|
||||
@@ -202,7 +171,6 @@ describe("UnifiedUpgradePromptService", () => {
|
||||
|
||||
it("should show dialog when all conditions are met", async () => {
|
||||
//Arrange
|
||||
mockConfigService.getFeatureFlag$.mockReturnValue(of(true));
|
||||
mockBillingService.hasPremiumFromAnySource$.mockReturnValue(of(false));
|
||||
mockOrganizationService.memberOrganizations$.mockReturnValue(of([]));
|
||||
const recentDate = new Date();
|
||||
@@ -224,7 +192,6 @@ describe("UnifiedUpgradePromptService", () => {
|
||||
|
||||
it("should not show dialog when account is null/undefined", async () => {
|
||||
// Arrange
|
||||
mockConfigService.getFeatureFlag$.mockReturnValue(of(true));
|
||||
accountSubject.next(null); // Set account to null
|
||||
setupTestService();
|
||||
|
||||
@@ -238,7 +205,6 @@ describe("UnifiedUpgradePromptService", () => {
|
||||
|
||||
it("should not show dialog when profile creation date is unavailable", async () => {
|
||||
// Arrange
|
||||
mockConfigService.getFeatureFlag$.mockReturnValue(of(true));
|
||||
mockBillingService.hasPremiumFromAnySource$.mockReturnValue(of(false));
|
||||
mockOrganizationService.memberOrganizations$.mockReturnValue(of([]));
|
||||
mockVaultProfileService.getProfileCreationDate.mockResolvedValue(null);
|
||||
@@ -256,7 +222,6 @@ describe("UnifiedUpgradePromptService", () => {
|
||||
|
||||
it("should not show dialog when running in self-hosted environment", async () => {
|
||||
// Arrange
|
||||
mockConfigService.getFeatureFlag$.mockReturnValue(of(true));
|
||||
mockOrganizationService.memberOrganizations$.mockReturnValue(of([]));
|
||||
mockBillingService.hasPremiumFromAnySource$.mockReturnValue(of(false));
|
||||
const recentDate = new Date();
|
||||
@@ -275,7 +240,6 @@ describe("UnifiedUpgradePromptService", () => {
|
||||
|
||||
it("should not show dialog when user has previously dismissed the modal", async () => {
|
||||
// Arrange
|
||||
mockConfigService.getFeatureFlag$.mockReturnValue(of(true));
|
||||
mockBillingService.hasPremiumFromAnySource$.mockReturnValue(of(false));
|
||||
mockOrganizationService.memberOrganizations$.mockReturnValue(of([]));
|
||||
const recentDate = new Date();
|
||||
@@ -295,7 +259,6 @@ describe("UnifiedUpgradePromptService", () => {
|
||||
|
||||
it("should save dismissal state when user closes the dialog", async () => {
|
||||
// Arrange
|
||||
mockConfigService.getFeatureFlag$.mockReturnValue(of(true));
|
||||
mockBillingService.hasPremiumFromAnySource$.mockReturnValue(of(false));
|
||||
mockOrganizationService.memberOrganizations$.mockReturnValue(of([]));
|
||||
const recentDate = new Date();
|
||||
@@ -320,7 +283,6 @@ describe("UnifiedUpgradePromptService", () => {
|
||||
|
||||
it("should not save dismissal state when user upgrades to premium", async () => {
|
||||
// Arrange
|
||||
mockConfigService.getFeatureFlag$.mockReturnValue(of(true));
|
||||
mockBillingService.hasPremiumFromAnySource$.mockReturnValue(of(false));
|
||||
mockOrganizationService.memberOrganizations$.mockReturnValue(of([]));
|
||||
const recentDate = new Date();
|
||||
|
||||
@@ -6,8 +6,6 @@ import { VaultProfileService } from "@bitwarden/angular/vault/services/vault-pro
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { SyncService } from "@bitwarden/common/platform/sync/sync.service";
|
||||
@@ -38,7 +36,6 @@ export class UnifiedUpgradePromptService {
|
||||
private unifiedUpgradeDialogRef: DialogRef<UnifiedUpgradeDialogResult> | null = null;
|
||||
constructor(
|
||||
private accountService: AccountService,
|
||||
private configService: ConfigService,
|
||||
private billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||
private vaultProfileService: VaultProfileService,
|
||||
private syncService: SyncService,
|
||||
@@ -70,26 +67,13 @@ export class UnifiedUpgradePromptService {
|
||||
isProfileLessThanFiveMinutesOld$,
|
||||
hasOrganizations$,
|
||||
this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id),
|
||||
this.configService.getFeatureFlag$(FeatureFlag.PM24996_ImplementUpgradeFromFreeDialog),
|
||||
hasDismissedModal$,
|
||||
]).pipe(
|
||||
map(
|
||||
([
|
||||
isProfileLessThanFiveMinutesOld,
|
||||
hasOrganizations,
|
||||
hasPremium,
|
||||
isFlagEnabled,
|
||||
hasDismissed,
|
||||
]) => {
|
||||
return (
|
||||
isProfileLessThanFiveMinutesOld &&
|
||||
!hasOrganizations &&
|
||||
!hasPremium &&
|
||||
isFlagEnabled &&
|
||||
!hasDismissed
|
||||
);
|
||||
},
|
||||
),
|
||||
map(([isProfileLessThanFiveMinutesOld, hasOrganizations, hasPremium, hasDismissed]) => {
|
||||
return (
|
||||
isProfileLessThanFiveMinutesOld && !hasOrganizations && !hasPremium && !hasDismissed
|
||||
);
|
||||
}),
|
||||
);
|
||||
}),
|
||||
take(1),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { TestBed } from "@angular/core/testing";
|
||||
import { BehaviorSubject, firstValueFrom, take, timeout } from "rxjs";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
|
||||
import { AuthRequestServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
@@ -17,11 +17,7 @@ import {
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
|
||||
import {
|
||||
PREMIUM_BANNER_REPROMPT_KEY,
|
||||
VaultBannersService,
|
||||
VisibleVaultBanner,
|
||||
} from "./vault-banners.service";
|
||||
import { VaultBannersService, VisibleVaultBanner } from "./vault-banners.service";
|
||||
|
||||
describe("VaultBannersService", () => {
|
||||
let service: VaultBannersService;
|
||||
@@ -79,101 +75,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("OutdatedBrowser", () => {
|
||||
beforeEach(async () => {
|
||||
// Hardcode `MSIE` in userAgent string
|
||||
|
||||
@@ -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 } from "@bitwarden/auth/common";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
@@ -7,7 +7,6 @@ import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abs
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import {
|
||||
StateProvider,
|
||||
PREMIUM_BANNER_DISK_LOCAL,
|
||||
BANNERS_DISMISSED_DISK,
|
||||
UserKeyDefinition,
|
||||
SingleUserState,
|
||||
@@ -18,30 +17,14 @@ import { UnionOfValues } from "@bitwarden/common/vault/types/union-of-values";
|
||||
|
||||
export const VisibleVaultBanner = {
|
||||
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,
|
||||
@@ -76,33 +59,6 @@ export class VaultBannersService {
|
||||
return pendingAuthRequests.length > 0 && !alreadyDismissed;
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -128,23 +84,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];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -161,42 +105,4 @@ export class VaultBannersService {
|
||||
// use nullish coalescing to default to an empty array
|
||||
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(),
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,15 +44,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";
|
||||
@@ -8,15 +7,13 @@ 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 { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
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";
|
||||
@@ -30,11 +27,9 @@ describe("VaultBannersComponent", () => {
|
||||
let messageSubject: Subject<{ command: string }>;
|
||||
const premiumBanner$ = new BehaviorSubject<boolean>(false);
|
||||
const pendingAuthRequest$ = new BehaviorSubject<boolean>(false);
|
||||
const PM24996_ImplementUpgradeFromFreeDialogFlag$ = 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(),
|
||||
shouldShowPendingAuthRequestBanner: jest.fn((userId: UserId) =>
|
||||
@@ -88,17 +83,6 @@ describe("VaultBannersComponent", () => {
|
||||
allMessages$: messageSubject.asObservable(),
|
||||
}),
|
||||
},
|
||||
{
|
||||
provide: ConfigService,
|
||||
useValue: mock<ConfigService>({
|
||||
getFeatureFlag$: jest.fn((flag: FeatureFlag) => {
|
||||
if (flag === FeatureFlag.PM24996_ImplementUpgradeFromFreeDialog) {
|
||||
return PM24996_ImplementUpgradeFromFreeDialogFlag$;
|
||||
}
|
||||
return new BehaviorSubject(false);
|
||||
}),
|
||||
}),
|
||||
},
|
||||
],
|
||||
})
|
||||
.overrideProvider(VaultBannersService, { useValue: bannerService })
|
||||
@@ -112,53 +96,6 @@ describe("VaultBannersComponent", () => {
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
describe("premiumBannerVisible$", () => {
|
||||
beforeEach(() => {
|
||||
// Reset feature flag to default (false) before each test
|
||||
PM24996_ImplementUpgradeFromFreeDialogFlag$.next(false);
|
||||
});
|
||||
|
||||
it("shows premium banner when shouldShowPremiumBanner is true and feature flag is off", async () => {
|
||||
premiumBanner$.next(true);
|
||||
PM24996_ImplementUpgradeFromFreeDialogFlag$.next(false);
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
const banner = fixture.debugElement.query(By.directive(BannerComponent));
|
||||
expect(banner.componentInstance.bannerType()).toBe("premium");
|
||||
});
|
||||
|
||||
it("hides premium banner when feature flag is enabled", async () => {
|
||||
premiumBanner$.next(true);
|
||||
PM24996_ImplementUpgradeFromFreeDialogFlag$.next(true);
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
const banner = fixture.debugElement.query(By.directive(BannerComponent));
|
||||
expect(banner).toBeNull();
|
||||
});
|
||||
|
||||
it("dismisses premium banner when shouldShowPremiumBanner is false", async () => {
|
||||
premiumBanner$.next(false);
|
||||
PM24996_ImplementUpgradeFromFreeDialogFlag$.next(false);
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
const banner = fixture.debugElement.query(By.directive(BannerComponent));
|
||||
expect(banner).toBeNull();
|
||||
});
|
||||
|
||||
it("hides premium banner when both shouldShowPremiumBanner is false and feature flag is enabled", async () => {
|
||||
premiumBanner$.next(false);
|
||||
PM24996_ImplementUpgradeFromFreeDialogFlag$.next(true);
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
const banner = fixture.debugElement.query(By.directive(BannerComponent));
|
||||
expect(banner).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("determineVisibleBanner", () => {
|
||||
[
|
||||
{
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
import { Component, Input, OnInit } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { Router } from "@angular/router";
|
||||
import { combineLatest, 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";
|
||||
|
||||
@@ -32,7 +29,6 @@ import { VaultBannersService, VisibleVaultBanner } from "./services/vault-banner
|
||||
})
|
||||
export class VaultBannersComponent implements OnInit {
|
||||
visibleBanners: VisibleVaultBanner[] = [];
|
||||
premiumBannerVisible$: Observable<boolean>;
|
||||
VisibleVaultBanner = VisibleVaultBanner;
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@@ -45,23 +41,7 @@ export class VaultBannersComponent implements OnInit {
|
||||
private router: Router,
|
||||
private accountService: AccountService,
|
||||
private messageListener: MessageListener,
|
||||
private configService: ConfigService,
|
||||
) {
|
||||
this.premiumBannerVisible$ = this.activeUserId$.pipe(
|
||||
filter((userId): userId is UserId => userId != null),
|
||||
switchMap((userId) =>
|
||||
combineLatest([
|
||||
this.vaultBannerService.shouldShowPremiumBanner$(userId),
|
||||
this.configService.getFeatureFlag$(FeatureFlag.PM24996_ImplementUpgradeFromFreeDialog),
|
||||
]).pipe(
|
||||
map(
|
||||
([shouldShowBanner, PM24996_ImplementUpgradeFromFreeDialogEnabled]) =>
|
||||
shouldShowBanner && !PM24996_ImplementUpgradeFromFreeDialogEnabled,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Listen for auth request messages and show banner immediately
|
||||
this.messageListener.allMessages$
|
||||
.pipe(
|
||||
|
||||
@@ -27,7 +27,6 @@ export enum FeatureFlag {
|
||||
TrialPaymentOptional = "PM-8163-trial-payment",
|
||||
PM24032_NewNavigationPremiumUpgradeButton = "pm-24032-new-navigation-premium-upgrade-button",
|
||||
PM25379_UseNewOrganizationMetadataStructure = "pm-25379-use-new-organization-metadata-structure",
|
||||
PM24996_ImplementUpgradeFromFreeDialog = "pm-24996-implement-upgrade-from-free-dialog",
|
||||
PM26793_FetchPremiumPriceFromPricingService = "pm-26793-fetch-premium-price-from-pricing-service",
|
||||
PM23713_PremiumBadgeOpensNewPremiumUpgradeDialog = "pm-23713-premium-badge-opens-new-premium-upgrade-dialog",
|
||||
PM26462_Milestone_3 = "pm-26462-milestone-3",
|
||||
@@ -137,7 +136,6 @@ export const DefaultFeatureFlagValue = {
|
||||
[FeatureFlag.TrialPaymentOptional]: FALSE,
|
||||
[FeatureFlag.PM24032_NewNavigationPremiumUpgradeButton]: FALSE,
|
||||
[FeatureFlag.PM25379_UseNewOrganizationMetadataStructure]: FALSE,
|
||||
[FeatureFlag.PM24996_ImplementUpgradeFromFreeDialog]: FALSE,
|
||||
[FeatureFlag.PM26793_FetchPremiumPriceFromPricingService]: FALSE,
|
||||
[FeatureFlag.PM23713_PremiumBadgeOpensNewPremiumUpgradeDialog]: FALSE,
|
||||
[FeatureFlag.PM26462_Milestone_3]: FALSE,
|
||||
|
||||
@@ -198,9 +198,7 @@ export const CIPHERS_DISK_LOCAL = new StateDefinition("ciphersLocal", "disk", {
|
||||
export const CIPHERS_MEMORY = new StateDefinition("ciphersMemory", "memory", {
|
||||
browser: "memory-large-object",
|
||||
});
|
||||
export const PREMIUM_BANNER_DISK_LOCAL = new StateDefinition("premiumBannerReprompt", "disk", {
|
||||
web: "disk-local",
|
||||
});
|
||||
|
||||
export const BANNERS_DISMISSED_DISK = new StateDefinition("bannersDismissed", "disk");
|
||||
export const VAULT_APPEARANCE = new StateDefinition("vaultAppearance", "disk");
|
||||
export const SECURITY_TASKS_DISK = new StateDefinition("securityTasks", "disk");
|
||||
|
||||
Reference in New Issue
Block a user