mirror of
https://github.com/bitwarden/browser
synced 2025-12-06 00:13:28 +00:00
[AC-217] Migrate to Banner Component (#8899)
* convert premium card to banner component - create VaultBanners component that will handle all banner logic * move upgrade browser notice to banner component * refactor verify email component to use the banner component * add email banner to VaultBanners component * move low KDF message to banner component * remove unused KDF component * allow multiple banners to be displayed at once * use vault service to consolidate premium banner logic - Implement prompt thresholds for premium banner - Update dismiss logic to re-run visibility logic * update variable name * move all dismiss/show logic to vault banner service * rename tense of methods for readability * apply underline to send email button to match other banner actions * fix dark mode styling across banners * remove unused variable * use bitLink directive for styling rather than tailwind * move premium banner to a standalone observable * update bootstrap styles to tailwind * use new KDF service for vault banners * move the VerifyEmailComponent to a standalone component * convert premium banner to a singular observable * remove unneeded import * AC-2589 add unique id for each vault banner * AC-2588 poll sync service to only show premium banner after a sync * close subscription to syncCompleted$ after one emit * remove unneeded ReplaySubject
This commit is contained in:
@@ -1,11 +1,14 @@
|
|||||||
<div class="tw-rounded tw-border tw-border-solid tw-border-warning-600 tw-bg-background">
|
<bit-banner bannerType="warning" (onClose)="onDismiss.emit()">
|
||||||
<div class="tw-bg-warning-600 tw-px-5 tw-py-2.5 tw-font-bold tw-uppercase tw-text-contrast">
|
{{ "verifyEmailDesc" | i18n }}
|
||||||
<i class="bwi bwi-envelope bwi-fw" aria-hidden="true"></i> {{ "verifyEmail" | i18n }}
|
<button
|
||||||
</div>
|
id="sendBtn"
|
||||||
<div class="tw-p-5">
|
bitLink
|
||||||
<p>{{ "verifyEmailDesc" | i18n }}</p>
|
linkType="contrast"
|
||||||
<button id="sendBtn" bitButton type="button" block [bitAction]="send">
|
bitButton
|
||||||
{{ "sendEmail" | i18n }}
|
type="button"
|
||||||
</button>
|
buttonType="unstyled"
|
||||||
</div>
|
[bitAction]="send"
|
||||||
</div>
|
>
|
||||||
|
{{ "sendEmail" | i18n }}
|
||||||
|
</button>
|
||||||
|
</bit-banner>
|
||||||
|
|||||||
@@ -1,25 +1,29 @@
|
|||||||
|
import { CommonModule } from "@angular/common";
|
||||||
import { Component, EventEmitter, Output } from "@angular/core";
|
import { Component, EventEmitter, Output } from "@angular/core";
|
||||||
|
|
||||||
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService } from "@bitwarden/common/abstractions/api.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 { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
|
import { AsyncActionsModule, BannerModule, ButtonModule, LinkModule } from "@bitwarden/components";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
standalone: true,
|
||||||
selector: "app-verify-email",
|
selector: "app-verify-email",
|
||||||
templateUrl: "verify-email.component.html",
|
templateUrl: "verify-email.component.html",
|
||||||
|
imports: [AsyncActionsModule, BannerModule, ButtonModule, CommonModule, JslibModule, LinkModule],
|
||||||
})
|
})
|
||||||
export class VerifyEmailComponent {
|
export class VerifyEmailComponent {
|
||||||
actionPromise: Promise<unknown>;
|
actionPromise: Promise<unknown>;
|
||||||
|
|
||||||
@Output() onVerified = new EventEmitter<boolean>();
|
@Output() onVerified = new EventEmitter<boolean>();
|
||||||
|
@Output() onDismiss = new EventEmitter<void>();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private apiService: ApiService,
|
private apiService: ApiService,
|
||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
private platformUtilsService: PlatformUtilsService,
|
private platformUtilsService: PlatformUtilsService,
|
||||||
private logService: LogService,
|
|
||||||
private tokenService: TokenService,
|
private tokenService: TokenService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
<div class="tw-rounded tw-border tw-border-solid tw-border-warning-600 tw-bg-background">
|
|
||||||
<div class="tw-bg-warning-600 tw-px-5 tw-py-2.5 tw-font-bold tw-uppercase tw-text-contrast">
|
|
||||||
<i class="bwi bwi-exclamation-triangle bwi-fw" aria-hidden="true"></i>
|
|
||||||
{{ "lowKdfIterations" | i18n }}
|
|
||||||
</div>
|
|
||||||
<div class="tw-p-5">
|
|
||||||
<p>{{ "updateLowKdfIterationsDesc" | i18n }}</p>
|
|
||||||
<a
|
|
||||||
bitButton
|
|
||||||
buttonType="secondary"
|
|
||||||
[block]="true"
|
|
||||||
routerLink="/settings/security/security-keys"
|
|
||||||
>
|
|
||||||
{{ "updateKdfSettings" | i18n }}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import { Component } from "@angular/core";
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: "app-low-kdf",
|
|
||||||
templateUrl: "low-kdf.component.html",
|
|
||||||
})
|
|
||||||
export class LowKdfComponent {}
|
|
||||||
@@ -53,7 +53,6 @@ import { TwoFactorSetupComponent } from "../auth/settings/two-factor-setup.compo
|
|||||||
import { TwoFactorVerifyComponent } from "../auth/settings/two-factor-verify.component";
|
import { TwoFactorVerifyComponent } from "../auth/settings/two-factor-verify.component";
|
||||||
import { TwoFactorWebAuthnComponent } from "../auth/settings/two-factor-webauthn.component";
|
import { TwoFactorWebAuthnComponent } from "../auth/settings/two-factor-webauthn.component";
|
||||||
import { TwoFactorYubiKeyComponent } from "../auth/settings/two-factor-yubikey.component";
|
import { TwoFactorYubiKeyComponent } from "../auth/settings/two-factor-yubikey.component";
|
||||||
import { VerifyEmailComponent } from "../auth/settings/verify-email.component";
|
|
||||||
import { UserVerificationModule } from "../auth/shared/components/user-verification";
|
import { UserVerificationModule } from "../auth/shared/components/user-verification";
|
||||||
import { SsoComponent } from "../auth/sso.component";
|
import { SsoComponent } from "../auth/sso.component";
|
||||||
import { TwoFactorOptionsComponent } from "../auth/two-factor-options.component";
|
import { TwoFactorOptionsComponent } from "../auth/two-factor-options.component";
|
||||||
@@ -70,7 +69,6 @@ import { HeaderModule } from "../layouts/header/header.module";
|
|||||||
import { ProductSwitcherModule } from "../layouts/product-switcher/product-switcher.module";
|
import { ProductSwitcherModule } from "../layouts/product-switcher/product-switcher.module";
|
||||||
import { UserLayoutComponent } from "../layouts/user-layout.component";
|
import { UserLayoutComponent } from "../layouts/user-layout.component";
|
||||||
import { DomainRulesComponent } from "../settings/domain-rules.component";
|
import { DomainRulesComponent } from "../settings/domain-rules.component";
|
||||||
import { LowKdfComponent } from "../settings/low-kdf.component";
|
|
||||||
import { PreferencesComponent } from "../settings/preferences.component";
|
import { PreferencesComponent } from "../settings/preferences.component";
|
||||||
import { VaultTimeoutInputComponent } from "../settings/vault-timeout-input.component";
|
import { VaultTimeoutInputComponent } from "../settings/vault-timeout-input.component";
|
||||||
import { GeneratorComponent } from "../tools/generator.component";
|
import { GeneratorComponent } from "../tools/generator.component";
|
||||||
@@ -186,11 +184,9 @@ import { SharedModule } from "./shared.module";
|
|||||||
UpdatePasswordComponent,
|
UpdatePasswordComponent,
|
||||||
UpdateTempPasswordComponent,
|
UpdateTempPasswordComponent,
|
||||||
VaultTimeoutInputComponent,
|
VaultTimeoutInputComponent,
|
||||||
VerifyEmailComponent,
|
|
||||||
VerifyEmailTokenComponent,
|
VerifyEmailTokenComponent,
|
||||||
VerifyRecoverDeleteComponent,
|
VerifyRecoverDeleteComponent,
|
||||||
VerifyRecoverDeleteProviderComponent,
|
VerifyRecoverDeleteProviderComponent,
|
||||||
LowKdfComponent,
|
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
UserVerificationModule,
|
UserVerificationModule,
|
||||||
@@ -264,11 +260,9 @@ import { SharedModule } from "./shared.module";
|
|||||||
UpdateTempPasswordComponent,
|
UpdateTempPasswordComponent,
|
||||||
UserLayoutComponent,
|
UserLayoutComponent,
|
||||||
VaultTimeoutInputComponent,
|
VaultTimeoutInputComponent,
|
||||||
VerifyEmailComponent,
|
|
||||||
VerifyEmailTokenComponent,
|
VerifyEmailTokenComponent,
|
||||||
VerifyRecoverDeleteComponent,
|
VerifyRecoverDeleteComponent,
|
||||||
VerifyRecoverDeleteProviderComponent,
|
VerifyRecoverDeleteProviderComponent,
|
||||||
LowKdfComponent,
|
|
||||||
HeaderModule,
|
HeaderModule,
|
||||||
DangerZoneComponent,
|
DangerZoneComponent,
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -0,0 +1,265 @@
|
|||||||
|
import { TestBed } from "@angular/core/testing";
|
||||||
|
import { BehaviorSubject, firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
|
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.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 { KdfType } from "@bitwarden/common/platform/enums";
|
||||||
|
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||||
|
import { FakeStateProvider, mockAccountServiceWith } from "@bitwarden/common/spec";
|
||||||
|
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";
|
||||||
|
|
||||||
|
describe("VaultBannersService", () => {
|
||||||
|
let service: VaultBannersService;
|
||||||
|
const isSelfHost = jest.fn().mockReturnValue(false);
|
||||||
|
const hasPremiumFromAnySource$ = new BehaviorSubject<boolean>(false);
|
||||||
|
const fakeStateProvider = new FakeStateProvider(mockAccountServiceWith("user-id" as 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);
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.useFakeTimers();
|
||||||
|
getLastSync.mockClear().mockResolvedValue(new Date("2024-05-14"));
|
||||||
|
isSelfHost.mockClear();
|
||||||
|
getEmailVerified.mockClear().mockResolvedValue(true);
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
providers: [
|
||||||
|
VaultBannersService,
|
||||||
|
{
|
||||||
|
provide: PlatformUtilsService,
|
||||||
|
useValue: { isSelfHost },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: BillingAccountProfileStateService,
|
||||||
|
useValue: { hasPremiumFromAnySource$: hasPremiumFromAnySource$ },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: StateProvider,
|
||||||
|
useValue: fakeStateProvider,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: PlatformUtilsService,
|
||||||
|
useValue: { isSelfHost },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: TokenService,
|
||||||
|
useValue: { getEmailVerified },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: UserVerificationService,
|
||||||
|
useValue: { hasMasterPassword },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: KdfConfigService,
|
||||||
|
useValue: { getKdfConfig },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: SyncService,
|
||||||
|
useValue: { getLastSync },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
service = TestBed.inject(VaultBannersService);
|
||||||
|
|
||||||
|
jest.advanceTimersByTime(201);
|
||||||
|
|
||||||
|
expect(await firstValueFrom(service.shouldShowPremiumBanner$)).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);
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
|
||||||
|
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(VisibleVaultBanner.Premium);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("updates state on first dismiss", async () => {
|
||||||
|
const state = await firstValueFrom(
|
||||||
|
fakeStateProvider.getActive(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.getActive(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.getActive(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 () => {
|
||||||
|
hasMasterPassword.mockResolvedValue(true);
|
||||||
|
getKdfConfig.mockResolvedValue({ kdfType: KdfType.PBKDF2_SHA256, iterations: 599999 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows low KDF iteration banner", async () => {
|
||||||
|
service = TestBed.inject(VaultBannersService);
|
||||||
|
|
||||||
|
expect(await service.shouldShowLowKDFBanner()).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 });
|
||||||
|
|
||||||
|
service = TestBed.inject(VaultBannersService);
|
||||||
|
|
||||||
|
expect(await service.shouldShowLowKDFBanner()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not show low KDF for iterations about 600,000", async () => {
|
||||||
|
getKdfConfig.mockResolvedValue({ kdfType: KdfType.PBKDF2_SHA256, iterations: 600001 });
|
||||||
|
|
||||||
|
service = TestBed.inject(VaultBannersService);
|
||||||
|
|
||||||
|
expect(await service.shouldShowLowKDFBanner()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("dismisses low KDF iteration banner", async () => {
|
||||||
|
service = TestBed.inject(VaultBannersService);
|
||||||
|
|
||||||
|
expect(await service.shouldShowLowKDFBanner()).toBe(true);
|
||||||
|
|
||||||
|
await service.dismissBanner(VisibleVaultBanner.KDFSettings);
|
||||||
|
|
||||||
|
expect(await service.shouldShowLowKDFBanner()).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("OutdatedBrowser", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
// Hardcode `MSIE` in userAgent string
|
||||||
|
const userAgent = "AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4.1 MSIE";
|
||||||
|
Object.defineProperty(navigator, "userAgent", {
|
||||||
|
configurable: true,
|
||||||
|
get: () => userAgent,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows outdated browser banner", async () => {
|
||||||
|
service = TestBed.inject(VaultBannersService);
|
||||||
|
|
||||||
|
expect(await service.shouldShowUpdateBrowserBanner()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("dismisses outdated browser banner", async () => {
|
||||||
|
service = TestBed.inject(VaultBannersService);
|
||||||
|
|
||||||
|
expect(await service.shouldShowUpdateBrowserBanner()).toBe(true);
|
||||||
|
|
||||||
|
await service.dismissBanner(VisibleVaultBanner.OutdatedBrowser);
|
||||||
|
|
||||||
|
expect(await service.shouldShowUpdateBrowserBanner()).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("VerifyEmail", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
getEmailVerified.mockResolvedValue(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows verify email banner", async () => {
|
||||||
|
service = TestBed.inject(VaultBannersService);
|
||||||
|
|
||||||
|
expect(await service.shouldShowVerifyEmailBanner()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("dismisses verify email banner", async () => {
|
||||||
|
service = TestBed.inject(VaultBannersService);
|
||||||
|
|
||||||
|
expect(await service.shouldShowVerifyEmailBanner()).toBe(true);
|
||||||
|
|
||||||
|
await service.dismissBanner(VisibleVaultBanner.VerifyEmail);
|
||||||
|
|
||||||
|
expect(await service.shouldShowVerifyEmailBanner()).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,215 @@
|
|||||||
|
import { Injectable } from "@angular/core";
|
||||||
|
import { Subject, Observable, combineLatest, firstValueFrom, map } from "rxjs";
|
||||||
|
import { mergeMap, take } from "rxjs/operators";
|
||||||
|
|
||||||
|
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.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 { KdfType, PBKDF2_ITERATIONS } from "@bitwarden/common/platform/enums";
|
||||||
|
import {
|
||||||
|
StateProvider,
|
||||||
|
ActiveUserState,
|
||||||
|
KeyDefinition,
|
||||||
|
PREMIUM_BANNER_DISK_LOCAL,
|
||||||
|
BANNERS_DISMISSED_DISK,
|
||||||
|
} from "@bitwarden/common/platform/state";
|
||||||
|
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||||
|
|
||||||
|
export enum VisibleVaultBanner {
|
||||||
|
KDFSettings = "kdf-settings",
|
||||||
|
OutdatedBrowser = "outdated-browser",
|
||||||
|
Premium = "premium",
|
||||||
|
VerifyEmail = "verify-email",
|
||||||
|
}
|
||||||
|
|
||||||
|
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, VisibleVaultBanner.Premium>;
|
||||||
|
|
||||||
|
export const PREMIUM_BANNER_REPROMPT_KEY = new KeyDefinition<PremiumBannerReprompt>(
|
||||||
|
PREMIUM_BANNER_DISK_LOCAL,
|
||||||
|
"bannerReprompt",
|
||||||
|
{
|
||||||
|
deserializer: (bannerReprompt) => bannerReprompt,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export const BANNERS_DISMISSED_DISK_KEY = new KeyDefinition<SessionBanners[]>(
|
||||||
|
BANNERS_DISMISSED_DISK,
|
||||||
|
"bannersDismissed",
|
||||||
|
{
|
||||||
|
deserializer: (bannersDismissed) => bannersDismissed,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
@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 stateProvider: StateProvider,
|
||||||
|
private billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||||
|
private platformUtilsService: PlatformUtilsService,
|
||||||
|
private kdfConfigService: KdfConfigService,
|
||||||
|
private syncService: SyncService,
|
||||||
|
) {
|
||||||
|
this.pollUntilSynced();
|
||||||
|
this.premiumBannerState = this.stateProvider.getActive(PREMIUM_BANNER_REPROMPT_KEY);
|
||||||
|
this.sessionBannerState = this.stateProvider.getActive(BANNERS_DISMISSED_DISK_KEY);
|
||||||
|
|
||||||
|
const premiumSources$ = combineLatest([
|
||||||
|
this.billingAccountProfileStateService.hasPremiumFromAnySource$,
|
||||||
|
this.premiumBannerState.state$,
|
||||||
|
]);
|
||||||
|
|
||||||
|
this.shouldShowPremiumBanner$ = this.syncCompleted$.pipe(
|
||||||
|
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(): Promise<boolean> {
|
||||||
|
const outdatedBrowser = window.navigator.userAgent.indexOf("MSIE") !== -1;
|
||||||
|
const alreadyDismissed = (await this.getBannerDismissedState()).includes(
|
||||||
|
VisibleVaultBanner.OutdatedBrowser,
|
||||||
|
);
|
||||||
|
|
||||||
|
return outdatedBrowser && !alreadyDismissed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns true when the verify email banner should be shown */
|
||||||
|
async shouldShowVerifyEmailBanner(): Promise<boolean> {
|
||||||
|
const needsVerification = !(await this.tokenService.getEmailVerified());
|
||||||
|
|
||||||
|
const alreadyDismissed = (await this.getBannerDismissedState()).includes(
|
||||||
|
VisibleVaultBanner.VerifyEmail,
|
||||||
|
);
|
||||||
|
|
||||||
|
return needsVerification && !alreadyDismissed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns true when the low KDF iteration banner should be shown */
|
||||||
|
async shouldShowLowKDFBanner(): Promise<boolean> {
|
||||||
|
const hasLowKDF = (await this.userVerificationService.hasMasterPassword())
|
||||||
|
? await this.isLowKdfIteration()
|
||||||
|
: false;
|
||||||
|
|
||||||
|
const alreadyDismissed = (await this.getBannerDismissedState()).includes(
|
||||||
|
VisibleVaultBanner.KDFSettings,
|
||||||
|
);
|
||||||
|
|
||||||
|
return hasLowKDF && !alreadyDismissed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Dismiss the given banner and perform any respective side effects */
|
||||||
|
async dismissBanner(banner: SessionBanners): Promise<void> {
|
||||||
|
if (banner === VisibleVaultBanner.Premium) {
|
||||||
|
await this.dismissPremiumBanner();
|
||||||
|
} else {
|
||||||
|
await this.sessionBannerState.update((current) => {
|
||||||
|
const bannersDismissed = current ?? [];
|
||||||
|
|
||||||
|
return [...bannersDismissed, banner];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns banners that have already been dismissed */
|
||||||
|
private async getBannerDismissedState(): 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$)) ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Increment dismissal state of the premium banner */
|
||||||
|
private async dismissPremiumBanner(): Promise<void> {
|
||||||
|
await this.premiumBannerState.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() {
|
||||||
|
const kdfConfig = await this.kdfConfigService.getKdfConfig();
|
||||||
|
return (
|
||||||
|
kdfConfig.kdfType === KdfType.PBKDF2_SHA256 &&
|
||||||
|
kdfConfig.iterations < PBKDF2_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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
<bit-banner
|
||||||
|
id="update-browser-banner"
|
||||||
|
class="-tw-m-6 tw-flex tw-flex-col tw-pb-6"
|
||||||
|
bannerType="warning"
|
||||||
|
*ngIf="visibleBanners.includes(VisibleVaultBanner.OutdatedBrowser)"
|
||||||
|
(onClose)="dismissBanner(VisibleVaultBanner.OutdatedBrowser)"
|
||||||
|
>
|
||||||
|
{{ "updateBrowserDesc" | i18n }}
|
||||||
|
<a
|
||||||
|
bitLink
|
||||||
|
linkType="contrast"
|
||||||
|
target="_blank"
|
||||||
|
href="https://browser-update.org/update-browser.html"
|
||||||
|
rel="noreferrer noopener"
|
||||||
|
>
|
||||||
|
{{ "updateBrowser" | i18n }}
|
||||||
|
</a>
|
||||||
|
</bit-banner>
|
||||||
|
|
||||||
|
<bit-banner
|
||||||
|
id="kdf-settings-banner"
|
||||||
|
class="-tw-m-6 tw-flex tw-flex-col tw-pb-6"
|
||||||
|
bannerType="warning"
|
||||||
|
*ngIf="visibleBanners.includes(VisibleVaultBanner.KDFSettings)"
|
||||||
|
(onClose)="dismissBanner(VisibleVaultBanner.KDFSettings)"
|
||||||
|
>
|
||||||
|
{{ "lowKDFIterationsBanner" | i18n }}
|
||||||
|
<a bitLink linkType="contrast" routerLink="/settings/security/security-keys">
|
||||||
|
{{ "changeKDFSettings" | i18n }}
|
||||||
|
</a>
|
||||||
|
</bit-banner>
|
||||||
|
|
||||||
|
<app-verify-email
|
||||||
|
id="verify-email-banner"
|
||||||
|
*ngIf="visibleBanners.includes(VisibleVaultBanner.VerifyEmail)"
|
||||||
|
class="-tw-m-6 tw-flex tw-flex-col tw-pb-6"
|
||||||
|
(onDismiss)="dismissBanner(VisibleVaultBanner.VerifyEmail)"
|
||||||
|
(onVerified)="dismissBanner(VisibleVaultBanner.VerifyEmail)"
|
||||||
|
></app-verify-email>
|
||||||
|
|
||||||
|
<bit-banner
|
||||||
|
id="premium-banner"
|
||||||
|
class="-tw-m-6 tw-flex tw-flex-col tw-pb-6"
|
||||||
|
bannerType="premium"
|
||||||
|
*ngIf="premiumBannerVisible$ | async"
|
||||||
|
(onClose)="dismissBanner(VisibleVaultBanner.Premium)"
|
||||||
|
>
|
||||||
|
{{ "premiumUpgradeUnlockFeatures" | i18n }}
|
||||||
|
<a bitLink linkType="contrast" routerLink="/settings/subscription/premium">
|
||||||
|
{{ "goPremium" | i18n }}
|
||||||
|
</a>
|
||||||
|
</bit-banner>
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||||
|
import { By } from "@angular/platform-browser";
|
||||||
|
import { mock } from "jest-mock-extended";
|
||||||
|
import { BehaviorSubject } from "rxjs";
|
||||||
|
|
||||||
|
import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe";
|
||||||
|
import { ApiService } from "@bitwarden/common/abstractions/api.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 { BannerComponent, BannerModule } from "@bitwarden/components";
|
||||||
|
|
||||||
|
import { VerifyEmailComponent } from "../../../auth/settings/verify-email.component";
|
||||||
|
import { LooseComponentsModule } from "../../../shared";
|
||||||
|
|
||||||
|
import { VaultBannersService, VisibleVaultBanner } from "./services/vault-banners.service";
|
||||||
|
import { VaultBannersComponent } from "./vault-banners.component";
|
||||||
|
|
||||||
|
describe("VaultBannersComponent", () => {
|
||||||
|
let component: VaultBannersComponent;
|
||||||
|
let fixture: ComponentFixture<VaultBannersComponent>;
|
||||||
|
const premiumBanner$ = new BehaviorSubject<boolean>(false);
|
||||||
|
|
||||||
|
const bannerService = mock<VaultBannersService>({
|
||||||
|
shouldShowPremiumBanner$: premiumBanner$,
|
||||||
|
shouldShowUpdateBrowserBanner: jest.fn(),
|
||||||
|
shouldShowVerifyEmailBanner: jest.fn(),
|
||||||
|
shouldShowLowKDFBanner: jest.fn(),
|
||||||
|
dismissBanner: jest.fn(),
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
bannerService.shouldShowPremiumBanner$ = premiumBanner$;
|
||||||
|
bannerService.shouldShowUpdateBrowserBanner.mockResolvedValue(false);
|
||||||
|
bannerService.shouldShowVerifyEmailBanner.mockResolvedValue(false);
|
||||||
|
bannerService.shouldShowLowKDFBanner.mockResolvedValue(false);
|
||||||
|
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [BannerModule, LooseComponentsModule, VerifyEmailComponent],
|
||||||
|
declarations: [VaultBannersComponent, I18nPipe],
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: VaultBannersService,
|
||||||
|
useValue: bannerService,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: I18nService,
|
||||||
|
useValue: mock<I18nService>({ t: (key) => key }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: ApiService,
|
||||||
|
useValue: mock<ApiService>(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: PlatformUtilsService,
|
||||||
|
useValue: mock<PlatformUtilsService>(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: TokenService,
|
||||||
|
useValue: mock<TokenService>(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}).compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(VaultBannersComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
|
||||||
|
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", () => {
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: "OutdatedBrowser",
|
||||||
|
method: bannerService.shouldShowUpdateBrowserBanner,
|
||||||
|
banner: VisibleVaultBanner.OutdatedBrowser,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "VerifyEmail",
|
||||||
|
method: bannerService.shouldShowVerifyEmailBanner,
|
||||||
|
banner: VisibleVaultBanner.VerifyEmail,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "LowKDF",
|
||||||
|
method: bannerService.shouldShowLowKDFBanner,
|
||||||
|
banner: VisibleVaultBanner.KDFSettings,
|
||||||
|
},
|
||||||
|
].forEach(({ name, method, banner }) => {
|
||||||
|
describe(name, () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
method.mockResolvedValue(true);
|
||||||
|
|
||||||
|
await component.ngOnInit();
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`shows ${name} banner`, async () => {
|
||||||
|
expect(component.visibleBanners).toEqual([banner]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`dismisses ${name} banner`, async () => {
|
||||||
|
const dismissButton = fixture.debugElement.nativeElement.querySelector(
|
||||||
|
'button[biticonbutton="bwi-close"]',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Mock out the banner service returning false after dismissing
|
||||||
|
method.mockResolvedValue(false);
|
||||||
|
|
||||||
|
dismissButton.dispatchEvent(new Event("click"));
|
||||||
|
|
||||||
|
expect(bannerService.dismissBanner).toHaveBeenCalledWith(banner);
|
||||||
|
|
||||||
|
expect(component.visibleBanners).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { Component, OnInit } from "@angular/core";
|
||||||
|
import { Observable } from "rxjs";
|
||||||
|
|
||||||
|
import { VaultBannersService, VisibleVaultBanner } from "./services/vault-banners.service";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "app-vault-banners",
|
||||||
|
templateUrl: "./vault-banners.component.html",
|
||||||
|
})
|
||||||
|
export class VaultBannersComponent implements OnInit {
|
||||||
|
visibleBanners: VisibleVaultBanner[] = [];
|
||||||
|
premiumBannerVisible$: Observable<boolean>;
|
||||||
|
VisibleVaultBanner = VisibleVaultBanner;
|
||||||
|
|
||||||
|
constructor(private vaultBannerService: VaultBannersService) {
|
||||||
|
this.premiumBannerVisible$ = this.vaultBannerService.shouldShowPremiumBanner$;
|
||||||
|
}
|
||||||
|
|
||||||
|
async ngOnInit(): Promise<void> {
|
||||||
|
await this.determineVisibleBanners();
|
||||||
|
}
|
||||||
|
|
||||||
|
async dismissBanner(banner: VisibleVaultBanner): Promise<void> {
|
||||||
|
await this.vaultBannerService.dismissBanner(banner);
|
||||||
|
|
||||||
|
await this.determineVisibleBanners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 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();
|
||||||
|
|
||||||
|
this.visibleBanners = [
|
||||||
|
showBrowserOutdated ? VisibleVaultBanner.OutdatedBrowser : null,
|
||||||
|
showVerifyEmail ? VisibleVaultBanner.VerifyEmail : null,
|
||||||
|
showLowKdf ? VisibleVaultBanner.KDFSettings : null,
|
||||||
|
].filter(Boolean); // remove all falsy values, i.e. null
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
<app-vault-banners></app-vault-banners>
|
||||||
|
|
||||||
<app-vault-header
|
<app-vault-header
|
||||||
[filter]="filter"
|
[filter]="filter"
|
||||||
[loading]="refreshing && !performingInitialLoad"
|
[loading]="refreshing && !performingInitialLoad"
|
||||||
@@ -14,8 +16,8 @@
|
|||||||
<app-vault-onboarding [ciphers]="ciphers" [orgs]="allOrganizations" (onAddCipher)="addCipher()">
|
<app-vault-onboarding [ciphers]="ciphers" [orgs]="allOrganizations" (onAddCipher)="addCipher()">
|
||||||
</app-vault-onboarding>
|
</app-vault-onboarding>
|
||||||
|
|
||||||
<div class="row">
|
<div class="tw-flex tw-flex-row -tw-mx-2.5">
|
||||||
<div class="col-3">
|
<div class="tw-basis-1/4 tw-max-w-1/4 tw-px-2.5">
|
||||||
<div class="groupings">
|
<div class="groupings">
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<div class="inner-content">
|
<div class="inner-content">
|
||||||
@@ -30,7 +32,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div [ngClass]="{ 'col-6': isShowingCards, 'col-9': !isShowingCards }">
|
<div class="tw-basis-3/4 tw-max-w-3/4 tw-px-2.5">
|
||||||
<app-callout type="warning" *ngIf="activeFilter.isDeleted" icon="bwi-exclamation-triangle">
|
<app-callout type="warning" *ngIf="activeFilter.isDeleted" icon="bwi-exclamation-triangle">
|
||||||
{{ trashCleanupWarning }}
|
{{ trashCleanupWarning }}
|
||||||
</app-callout>
|
</app-callout>
|
||||||
@@ -81,44 +83,6 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-3">
|
|
||||||
<app-low-kdf class="d-block mb-4" *ngIf="showLowKdf"> </app-low-kdf>
|
|
||||||
|
|
||||||
<app-verify-email
|
|
||||||
*ngIf="showVerifyEmail"
|
|
||||||
class="d-block mb-4"
|
|
||||||
(onVerified)="emailVerified($event)"
|
|
||||||
></app-verify-email>
|
|
||||||
|
|
||||||
<div class="card border-warning mb-4" *ngIf="showBrowserOutdated">
|
|
||||||
<div class="card-header bg-warning text-white">
|
|
||||||
<i class="bwi bwi-exclamation-triangle bwi-fw" aria-hidden="true"></i>
|
|
||||||
{{ "updateBrowser" | i18n }}
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<p>{{ "updateBrowserDesc" | i18n }}</p>
|
|
||||||
<a
|
|
||||||
class="btn btn-block btn-outline-secondary"
|
|
||||||
target="_blank"
|
|
||||||
href="https://browser-update.org/update-browser.html"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
{{ "updateBrowser" | i18n }}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card border-success mb-4" *ngIf="showPremiumCallout">
|
|
||||||
<div class="card-header bg-success text-white">
|
|
||||||
<i class="bwi bwi-star-f bwi-fw" aria-hidden="true"></i> {{ "goPremium" | i18n }}
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<p>{{ "premiumUpgradeUnlockFeatures" | i18n }}</p>
|
|
||||||
<a class="btn btn-block btn-outline-secondary" routerLink="/settings/subscription/premium">
|
|
||||||
{{ "goPremium" | i18n }}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ng-template #attachments></ng-template>
|
<ng-template #attachments></ng-template>
|
||||||
|
|||||||
@@ -35,9 +35,6 @@ import { EventCollectionService } from "@bitwarden/common/abstractions/event/eve
|
|||||||
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||||
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.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 { EventType } from "@bitwarden/common/enums";
|
import { EventType } from "@bitwarden/common/enums";
|
||||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||||
@@ -47,7 +44,6 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
|
|||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { KdfType, PBKDF2_ITERATIONS } from "@bitwarden/common/platform/enums";
|
|
||||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service";
|
import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service";
|
||||||
@@ -122,10 +118,6 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
@ViewChild("collectionsModal", { read: ViewContainerRef, static: true })
|
@ViewChild("collectionsModal", { read: ViewContainerRef, static: true })
|
||||||
collectionsModalRef: ViewContainerRef;
|
collectionsModalRef: ViewContainerRef;
|
||||||
|
|
||||||
showVerifyEmail = false;
|
|
||||||
showBrowserOutdated = false;
|
|
||||||
showPremiumCallout = false;
|
|
||||||
showLowKdf = false;
|
|
||||||
trashCleanupWarning: string = null;
|
trashCleanupWarning: string = null;
|
||||||
kdfIterations: number;
|
kdfIterations: number;
|
||||||
activeFilter: VaultFilter = new VaultFilter();
|
activeFilter: VaultFilter = new VaultFilter();
|
||||||
@@ -161,7 +153,6 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
private modalService: ModalService,
|
private modalService: ModalService,
|
||||||
private dialogService: DialogService,
|
private dialogService: DialogService,
|
||||||
private tokenService: TokenService,
|
|
||||||
private messagingService: MessagingService,
|
private messagingService: MessagingService,
|
||||||
private platformUtilsService: PlatformUtilsService,
|
private platformUtilsService: PlatformUtilsService,
|
||||||
private broadcasterService: BroadcasterService,
|
private broadcasterService: BroadcasterService,
|
||||||
@@ -180,14 +171,11 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
private searchPipe: SearchPipe,
|
private searchPipe: SearchPipe,
|
||||||
private configService: ConfigService,
|
private configService: ConfigService,
|
||||||
private apiService: ApiService,
|
private apiService: ApiService,
|
||||||
private userVerificationService: UserVerificationService,
|
|
||||||
private billingAccountProfileStateService: BillingAccountProfileStateService,
|
private billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||||
private toastService: ToastService,
|
private toastService: ToastService,
|
||||||
protected kdfConfigService: KdfConfigService,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
this.showBrowserOutdated = window.navigator.userAgent.indexOf("MSIE") !== -1;
|
|
||||||
this.trashCleanupWarning = this.i18nService.t(
|
this.trashCleanupWarning = this.i18nService.t(
|
||||||
this.platformUtilsService.isSelfHost()
|
this.platformUtilsService.isSelfHost()
|
||||||
? "trashCleanupWarningSelfHosted"
|
? "trashCleanupWarningSelfHosted"
|
||||||
@@ -197,18 +185,8 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
const firstSetup$ = this.route.queryParams.pipe(
|
const firstSetup$ = this.route.queryParams.pipe(
|
||||||
first(),
|
first(),
|
||||||
switchMap(async (params: Params) => {
|
switchMap(async (params: Params) => {
|
||||||
this.showVerifyEmail = !(await this.tokenService.getEmailVerified());
|
|
||||||
this.showLowKdf = (await this.userVerificationService.hasMasterPassword())
|
|
||||||
? await this.isLowKdfIteration()
|
|
||||||
: false;
|
|
||||||
await this.syncService.fullSync(false);
|
await this.syncService.fullSync(false);
|
||||||
|
|
||||||
const canAccessPremium = await firstValueFrom(
|
|
||||||
this.billingAccountProfileStateService.hasPremiumFromAnySource$,
|
|
||||||
);
|
|
||||||
this.showPremiumCallout =
|
|
||||||
!this.showVerifyEmail && !canAccessPremium && !this.platformUtilsService.isSelfHost();
|
|
||||||
|
|
||||||
const cipherId = getCipherIdFromParams(params);
|
const cipherId = getCipherIdFromParams(params);
|
||||||
if (!cipherId) {
|
if (!cipherId) {
|
||||||
return;
|
return;
|
||||||
@@ -412,16 +390,6 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
get isShowingCards() {
|
|
||||||
return (
|
|
||||||
this.showBrowserOutdated || this.showPremiumCallout || this.showVerifyEmail || this.showLowKdf
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
emailVerified(verified: boolean) {
|
|
||||||
this.showVerifyEmail = !verified;
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnDestroy() {
|
ngOnDestroy() {
|
||||||
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
|
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
|
||||||
this.destroy$.next();
|
this.destroy$.next();
|
||||||
@@ -1005,14 +973,6 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
: this.cipherService.softDeleteWithServer(id);
|
: this.cipherService.softDeleteWithServer(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
async isLowKdfIteration() {
|
|
||||||
const kdfConfig = await this.kdfConfigService.getKdfConfig();
|
|
||||||
return (
|
|
||||||
kdfConfig.kdfType === KdfType.PBKDF2_SHA256 &&
|
|
||||||
kdfConfig.iterations < PBKDF2_ITERATIONS.defaultValue
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async repromptCipher(ciphers: CipherView[]) {
|
protected async repromptCipher(ciphers: CipherView[]) {
|
||||||
const notProtected = !ciphers.find((cipher) => cipher.reprompt !== CipherRepromptType.None);
|
const notProtected = !ciphers.find((cipher) => cipher.reprompt !== CipherRepromptType.None);
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { NgModule } from "@angular/core";
|
import { NgModule } from "@angular/core";
|
||||||
|
|
||||||
import { BreadcrumbsModule } from "@bitwarden/components";
|
import { BannerModule, BreadcrumbsModule } from "@bitwarden/components";
|
||||||
|
|
||||||
|
import { VerifyEmailComponent } from "../../auth/settings/verify-email.component";
|
||||||
import { LooseComponentsModule, SharedModule } from "../../shared";
|
import { LooseComponentsModule, SharedModule } from "../../shared";
|
||||||
import { CollectionDialogModule } from "../components/collection-dialog";
|
import { CollectionDialogModule } from "../components/collection-dialog";
|
||||||
import { VaultItemsModule } from "../components/vault-items/vault-items.module";
|
import { VaultItemsModule } from "../components/vault-items/vault-items.module";
|
||||||
@@ -11,6 +12,8 @@ import { GroupBadgeModule } from "../org-vault/group-badge/group-badge.module";
|
|||||||
import { BulkDialogsModule } from "./bulk-action-dialogs/bulk-dialogs.module";
|
import { BulkDialogsModule } from "./bulk-action-dialogs/bulk-dialogs.module";
|
||||||
import { OrganizationBadgeModule } from "./organization-badge/organization-badge.module";
|
import { OrganizationBadgeModule } from "./organization-badge/organization-badge.module";
|
||||||
import { PipesModule } from "./pipes/pipes.module";
|
import { PipesModule } from "./pipes/pipes.module";
|
||||||
|
import { VaultBannersService } from "./vault-banners/services/vault-banners.service";
|
||||||
|
import { VaultBannersComponent } from "./vault-banners/vault-banners.component";
|
||||||
import { VaultFilterModule } from "./vault-filter/vault-filter.module";
|
import { VaultFilterModule } from "./vault-filter/vault-filter.module";
|
||||||
import { VaultHeaderComponent } from "./vault-header/vault-header.component";
|
import { VaultHeaderComponent } from "./vault-header/vault-header.component";
|
||||||
import { VaultOnboardingService as VaultOnboardingServiceAbstraction } from "./vault-onboarding/services/abstraction/vault-onboarding.service";
|
import { VaultOnboardingService as VaultOnboardingServiceAbstraction } from "./vault-onboarding/services/abstraction/vault-onboarding.service";
|
||||||
@@ -34,10 +37,13 @@ import { VaultComponent } from "./vault.component";
|
|||||||
VaultItemsModule,
|
VaultItemsModule,
|
||||||
CollectionDialogModule,
|
CollectionDialogModule,
|
||||||
VaultOnboardingComponent,
|
VaultOnboardingComponent,
|
||||||
|
BannerModule,
|
||||||
|
VerifyEmailComponent,
|
||||||
],
|
],
|
||||||
declarations: [VaultComponent, VaultHeaderComponent],
|
declarations: [VaultComponent, VaultHeaderComponent, VaultBannersComponent],
|
||||||
exports: [VaultComponent],
|
exports: [VaultComponent],
|
||||||
providers: [
|
providers: [
|
||||||
|
VaultBannersService,
|
||||||
{
|
{
|
||||||
provide: VaultOnboardingServiceAbstraction,
|
provide: VaultOnboardingServiceAbstraction,
|
||||||
useClass: VaultOnboardingService,
|
useClass: VaultOnboardingService,
|
||||||
|
|||||||
@@ -8223,6 +8223,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"lowKDFIterationsBanner": {
|
||||||
|
"message": "Low KDF iterations. Increase your iterations to improve the security of your account."
|
||||||
|
},
|
||||||
|
"changeKDFSettings": {
|
||||||
|
"message": "Change KDF settings"
|
||||||
|
},
|
||||||
"secureYourInfrastructure": {
|
"secureYourInfrastructure": {
|
||||||
"message": "Secure your infrastructure"
|
"message": "Secure your infrastructure"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -160,3 +160,7 @@ export const CIPHERS_DISK_LOCAL = new StateDefinition("ciphersLocal", "disk", {
|
|||||||
export const CIPHERS_MEMORY = new StateDefinition("ciphersMemory", "memory", {
|
export const CIPHERS_MEMORY = new StateDefinition("ciphersMemory", "memory", {
|
||||||
browser: "memory-large-object",
|
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");
|
||||||
|
|||||||
Reference in New Issue
Block a user