1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-24 08:33:29 +00:00

[PM-31433] Welcome Dialog with Extension Prompt (#18849)

* add welcome prompt when extension is not installed

* add feature flag

* move prompt logic to internal service and add day prompt

* rename dialog component

* remove feature flag hardcode and add documentation

* use i18n for image alt

* move state into service

* be more explicit when the account or creation date is not available

* remove spaces

* fix types caused by introducing a numeric feature flag type

* add `typeof` for feature flag typing
This commit is contained in:
Nick Krantz
2026-02-20 10:23:59 -06:00
committed by GitHub
parent a7c74c6f76
commit a610ce01a2
14 changed files with 596 additions and 5 deletions

View File

@@ -51,7 +51,7 @@ import type { NativeWindowObject } from "./desktop-fido2-user-interface.service"
export class DesktopAutofillService implements OnDestroy {
private destroy$ = new Subject<void>();
private registrationRequest: autofill.PasskeyRegistrationRequest;
private featureFlag?: FeatureFlag;
private featureFlag?: typeof FeatureFlag.MacOsNativeCredentialSync;
private isEnabled: boolean = false;
constructor(

View File

@@ -105,7 +105,7 @@ class MockBillingAccountProfileStateService implements Partial<BillingAccountPro
class MockConfigService implements Partial<ConfigService> {
getFeatureFlag$<Flag extends FeatureFlag>(key: Flag): Observable<FeatureFlagValueType<Flag>> {
return of(false);
return of(false as FeatureFlagValueType<Flag>);
}
}

View File

@@ -100,7 +100,7 @@ class MockBillingAccountProfileStateService implements Partial<BillingAccountPro
class MockConfigService implements Partial<ConfigService> {
getFeatureFlag$<Flag extends FeatureFlag>(key: Flag): Observable<FeatureFlagValueType<Flag>> {
return of(false);
return of(false as FeatureFlagValueType<Flag>);
}
}

View File

@@ -0,0 +1,34 @@
<div
class="tw-max-w-3xl tw-p-9 tw-border tw-border-solid tw-border-border-base tw-rounded-xl tw-bg-background tw-text-center"
>
<img
src="/images/vault/extension-mock-login.png"
[alt]="'extensionPromptImageAlt' | i18n"
class="tw-mb-[1.875rem]"
/>
<div class="tw-flex tw-flex-col tw-gap-[1.6875rem]">
<h2 class="tw-mb-0">
{{ "extensionPromptHeading" | i18n }}
</h2>
<p class="tw-mb-0">
{{ "extensionPromptBody" | i18n }}
</p>
<div class="tw-flex tw-gap-[1.125rem] tw-justify-center">
<button bitButton="secondary" type="button" (click)="dismissPrompt()">
{{ "skip" | i18n }}
</button>
<a
bitButton
buttonType="primary"
[href]="webStoreUrl"
target="_blank"
rel="noopener noreferrer"
(click)="dismissPrompt()"
>
{{ "downloadExtension" | i18n }}
<bit-icon name="bwi-external-link" class="tw-ml-[0.42rem]"></bit-icon>
</a>
</div>
</div>
</div>

View File

@@ -0,0 +1,86 @@
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { provideNoopAnimations } from "@angular/platform-browser/animations";
import { mock, MockProxy } from "jest-mock-extended";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { DeviceType } from "@bitwarden/common/enums";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { mockAccountServiceWith } from "@bitwarden/common/spec";
import { UserId } from "@bitwarden/common/types/guid";
import { DialogRef, DialogService } from "@bitwarden/components";
import { WebVaultExtensionPromptService } from "../../services/web-vault-extension-prompt.service";
import { WebVaultExtensionPromptDialogComponent } from "./web-vault-extension-prompt-dialog.component";
describe("WebVaultExtensionPromptDialogComponent", () => {
let component: WebVaultExtensionPromptDialogComponent;
let fixture: ComponentFixture<WebVaultExtensionPromptDialogComponent>;
let mockDialogRef: MockProxy<DialogRef<void>>;
const mockUserId = "test-user-id" as UserId;
const getDevice = jest.fn(() => DeviceType.ChromeBrowser);
const mockUpdate = jest.fn().mockResolvedValue(undefined);
const getDialogDismissedState = jest.fn().mockReturnValue({
update: mockUpdate,
});
beforeEach(async () => {
const mockAccountService = mockAccountServiceWith(mockUserId);
mockDialogRef = mock<DialogRef<void>>();
await TestBed.configureTestingModule({
imports: [WebVaultExtensionPromptDialogComponent],
providers: [
provideNoopAnimations(),
{
provide: PlatformUtilsService,
useValue: { getDevice },
},
{ provide: I18nService, useValue: { t: (key: string) => key } },
{ provide: AccountService, useValue: mockAccountService },
{ provide: DialogRef, useValue: mockDialogRef },
{ provide: DialogService, useValue: mock<DialogService>() },
{
provide: WebVaultExtensionPromptService,
useValue: { getDialogDismissedState },
},
],
}).compileComponents();
fixture = TestBed.createComponent(WebVaultExtensionPromptDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
describe("ngOnInit", () => {
it("sets webStoreUrl", () => {
expect(getDevice).toHaveBeenCalled();
expect(component["webStoreUrl"]).toBe(
"https://chromewebstore.google.com/detail/bitwarden-password-manage/nngceckbapebfimnlniiiahkandclblb",
);
});
});
describe("dismissPrompt", () => {
it("calls webVaultExtensionPromptService.getDialogDismissedState and updates to true", async () => {
await component.dismissPrompt();
expect(getDialogDismissedState).toHaveBeenCalledWith(mockUserId);
expect(mockUpdate).toHaveBeenCalledWith(expect.any(Function));
const updateFn = mockUpdate.mock.calls[0][0];
expect(updateFn()).toBe(true);
});
it("closes the dialog", async () => {
await component.dismissPrompt();
expect(mockDialogRef.close).toHaveBeenCalled();
});
});
});

View File

@@ -0,0 +1,51 @@
import { CommonModule } from "@angular/common";
import { Component, ChangeDetectionStrategy, OnInit } from "@angular/core";
import { firstValueFrom } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { getWebStoreUrl } from "@bitwarden/common/vault/utils/get-web-store-url";
import {
ButtonModule,
DialogModule,
DialogRef,
DialogService,
IconComponent,
} from "@bitwarden/components";
import { I18nPipe } from "@bitwarden/ui-common";
import { WebVaultExtensionPromptService } from "../../services/web-vault-extension-prompt.service";
@Component({
selector: "web-vault-extension-prompt-dialog",
templateUrl: "./web-vault-extension-prompt-dialog.component.html",
imports: [CommonModule, ButtonModule, DialogModule, I18nPipe, IconComponent],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class WebVaultExtensionPromptDialogComponent implements OnInit {
constructor(
private platformUtilsService: PlatformUtilsService,
private accountService: AccountService,
private dialogRef: DialogRef<void>,
private webVaultExtensionPromptService: WebVaultExtensionPromptService,
) {}
/** Download Url for the extension based on the browser */
protected webStoreUrl: string = "";
ngOnInit(): void {
this.webStoreUrl = getWebStoreUrl(this.platformUtilsService.getDevice());
}
async dismissPrompt() {
const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
await this.webVaultExtensionPromptService.getDialogDismissedState(userId).update(() => true);
this.dialogRef.close();
}
/** Opens the web extension prompt generator dialog. */
static open(dialogService: DialogService) {
return dialogService.open(WebVaultExtensionPromptDialogComponent);
}
}

View File

@@ -0,0 +1,269 @@
import { TestBed } from "@angular/core/testing";
import { BehaviorSubject } from "rxjs";
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 { UserId } from "@bitwarden/common/types/guid";
import { DialogService } from "@bitwarden/components";
import { StateProvider } from "@bitwarden/state";
import { WebVaultExtensionPromptDialogComponent } from "../components/web-vault-extension-prompt/web-vault-extension-prompt-dialog.component";
import { WebBrowserInteractionService } from "./web-browser-interaction.service";
import { WebVaultExtensionPromptService } from "./web-vault-extension-prompt.service";
describe("WebVaultExtensionPromptService", () => {
let service: WebVaultExtensionPromptService;
const mockUserId = "user-123" as UserId;
const mockAccountCreationDate = new Date("2026-01-15");
const getFeatureFlag = jest.fn();
const extensionInstalled$ = new BehaviorSubject<boolean>(false);
const mockStateSubject = new BehaviorSubject<boolean>(false);
const activeAccountSubject = new BehaviorSubject<{ id: UserId; creationDate: Date | null }>({
id: mockUserId,
creationDate: mockAccountCreationDate,
});
const getUser = jest.fn().mockReturnValue({ state$: mockStateSubject.asObservable() });
beforeEach(() => {
jest.clearAllMocks();
getFeatureFlag.mockResolvedValue(false);
extensionInstalled$.next(false);
mockStateSubject.next(false);
activeAccountSubject.next({ id: mockUserId, creationDate: mockAccountCreationDate });
TestBed.configureTestingModule({
providers: [
WebVaultExtensionPromptService,
{
provide: StateProvider,
useValue: {
getUser,
},
},
{
provide: WebBrowserInteractionService,
useValue: {
extensionInstalled$: extensionInstalled$.asObservable(),
},
},
{
provide: AccountService,
useValue: {
activeAccount$: activeAccountSubject.asObservable(),
},
},
{
provide: ConfigService,
useValue: {
getFeatureFlag,
},
},
{
provide: DialogService,
useValue: {
open: jest.fn(),
},
},
],
});
service = TestBed.inject(WebVaultExtensionPromptService);
});
describe("conditionallyPromptUserForExtension", () => {
it("returns false when feature flag is disabled", async () => {
getFeatureFlag.mockResolvedValueOnce(false);
const result = await service.conditionallyPromptUserForExtension(mockUserId);
expect(result).toBe(false);
expect(getFeatureFlag).toHaveBeenCalledWith(
FeatureFlag.PM29438_WelcomeDialogWithExtensionPrompt,
);
});
it("returns false when dialog has been dismissed", async () => {
getFeatureFlag.mockResolvedValueOnce(true);
mockStateSubject.next(true);
extensionInstalled$.next(false);
const result = await service.conditionallyPromptUserForExtension(mockUserId);
expect(result).toBe(false);
});
it("returns false when profile is not within thresholds (too old)", async () => {
getFeatureFlag
.mockResolvedValueOnce(true) // Main feature flag
.mockResolvedValueOnce(0); // Min age days
mockStateSubject.next(false);
extensionInstalled$.next(false);
const oldAccountDate = new Date("2025-12-01"); // More than 30 days old
activeAccountSubject.next({ id: mockUserId, creationDate: oldAccountDate });
const result = await service.conditionallyPromptUserForExtension(mockUserId);
expect(result).toBe(false);
});
it("returns false when profile is not within thresholds (too young)", async () => {
getFeatureFlag
.mockResolvedValueOnce(true) // Main feature flag
.mockResolvedValueOnce(10); // Min age days = 10
mockStateSubject.next(false);
extensionInstalled$.next(false);
const youngAccountDate = new Date(); // Today
youngAccountDate.setDate(youngAccountDate.getDate() - 5); // 5 days old
activeAccountSubject.next({ id: mockUserId, creationDate: youngAccountDate });
const result = await service.conditionallyPromptUserForExtension(mockUserId);
expect(result).toBe(false);
});
it("returns false when extension is installed", async () => {
getFeatureFlag
.mockResolvedValueOnce(true) // Main feature flag
.mockResolvedValueOnce(0); // Min age days
mockStateSubject.next(false);
extensionInstalled$.next(true);
const result = await service.conditionallyPromptUserForExtension(mockUserId);
expect(result).toBe(false);
});
it("returns true and opens dialog when all conditions are met", async () => {
getFeatureFlag
.mockResolvedValueOnce(true) // Main feature flag
.mockResolvedValueOnce(0); // Min age days
mockStateSubject.next(false);
extensionInstalled$.next(false);
// Set account creation date to be within threshold (15 days old)
const validCreationDate = new Date();
validCreationDate.setDate(validCreationDate.getDate() - 15);
activeAccountSubject.next({ id: mockUserId, creationDate: validCreationDate });
const dialogClosedSubject = new BehaviorSubject<void>(undefined);
const openSpy = jest
.spyOn(WebVaultExtensionPromptDialogComponent, "open")
.mockReturnValue({ closed: dialogClosedSubject.asObservable() } as any);
const resultPromise = service.conditionallyPromptUserForExtension(mockUserId);
// Close the dialog
dialogClosedSubject.next(undefined);
const result = await resultPromise;
expect(openSpy).toHaveBeenCalledWith(expect.anything());
expect(result).toBe(true);
});
});
describe("profileIsWithinThresholds", () => {
it("returns false when account is younger than min threshold", async () => {
const minAgeDays = 7;
getFeatureFlag.mockResolvedValueOnce(minAgeDays);
const recentDate = new Date();
recentDate.setDate(recentDate.getDate() - 5); // 5 days old
activeAccountSubject.next({ id: mockUserId, creationDate: recentDate });
const result = await service["profileIsWithinThresholds"]();
expect(result).toBe(false);
});
it("returns true when account is exactly at min threshold", async () => {
const minAgeDays = 7;
getFeatureFlag.mockResolvedValueOnce(minAgeDays);
const exactDate = new Date();
exactDate.setDate(exactDate.getDate() - 7); // Exactly 7 days old
activeAccountSubject.next({ id: mockUserId, creationDate: exactDate });
const result = await service["profileIsWithinThresholds"]();
expect(result).toBe(true);
});
it("returns true when account is within the thresholds", async () => {
const minAgeDays = 0;
getFeatureFlag.mockResolvedValueOnce(minAgeDays);
const validDate = new Date();
validDate.setDate(validDate.getDate() - 15); // 15 days old (between 0 and 30)
activeAccountSubject.next({ id: mockUserId, creationDate: validDate });
const result = await service["profileIsWithinThresholds"]();
expect(result).toBe(true);
});
it("returns false when account is older than max threshold (30 days)", async () => {
const minAgeDays = 0;
getFeatureFlag.mockResolvedValueOnce(minAgeDays);
const oldDate = new Date();
oldDate.setDate(oldDate.getDate() - 31); // 31 days old
activeAccountSubject.next({ id: mockUserId, creationDate: oldDate });
const result = await service["profileIsWithinThresholds"]();
expect(result).toBe(false);
});
it("returns false when account is exactly 30 days old", async () => {
const minAgeDays = 0;
getFeatureFlag.mockResolvedValueOnce(minAgeDays);
const exactDate = new Date();
exactDate.setDate(exactDate.getDate() - 30); // Exactly 30 days old
activeAccountSubject.next({ id: mockUserId, creationDate: exactDate });
const result = await service["profileIsWithinThresholds"]();
expect(result).toBe(false);
});
it("uses default min age of 0 when feature flag is null", async () => {
getFeatureFlag.mockResolvedValueOnce(null);
const recentDate = new Date();
recentDate.setDate(recentDate.getDate() - 5); // 5 days old
activeAccountSubject.next({ id: mockUserId, creationDate: recentDate });
const result = await service["profileIsWithinThresholds"]();
expect(result).toBe(true);
});
it("defaults to false", async () => {
getFeatureFlag.mockResolvedValueOnce(0);
activeAccountSubject.next({ id: mockUserId, creationDate: null });
const result = await service["profileIsWithinThresholds"]();
expect(result).toBe(false);
});
});
describe("getDialogDismissedState", () => {
it("returns the SingleUserState for the dialog dismissed state", () => {
service.getDialogDismissedState(mockUserId);
expect(getUser).toHaveBeenCalledWith(
mockUserId,
expect.objectContaining({
key: "vaultWelcomeExtensionDialogDismissed",
}),
);
});
});
});

View File

@@ -0,0 +1,104 @@
import { inject, Injectable } from "@angular/core";
import { firstValueFrom, map } from "rxjs";
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 { UserId } from "@bitwarden/common/types/guid";
import { DialogService } from "@bitwarden/components";
import { StateProvider, UserKeyDefinition, WELCOME_EXTENSION_DIALOG_DISK } from "@bitwarden/state";
import { WebVaultExtensionPromptDialogComponent } from "../components/web-vault-extension-prompt/web-vault-extension-prompt-dialog.component";
import { WebBrowserInteractionService } from "./web-browser-interaction.service";
export const WELCOME_EXTENSION_DIALOG_DISMISSED = new UserKeyDefinition<boolean>(
WELCOME_EXTENSION_DIALOG_DISK,
"vaultWelcomeExtensionDialogDismissed",
{
deserializer: (dismissed) => dismissed,
clearOn: [],
},
);
@Injectable({ providedIn: "root" })
export class WebVaultExtensionPromptService {
private stateProvider = inject(StateProvider);
private webBrowserInteractionService = inject(WebBrowserInteractionService);
private accountService = inject(AccountService);
private configService = inject(ConfigService);
private dialogService = inject(DialogService);
/**
* Conditionally prompts the user to install the web extension
*/
async conditionallyPromptUserForExtension(userId: UserId) {
const featureFlagEnabled = await this.configService.getFeatureFlag(
FeatureFlag.PM29438_WelcomeDialogWithExtensionPrompt,
);
if (!featureFlagEnabled) {
return false;
}
// Extension check takes time, trigger it early
const hasExtensionInstalled = firstValueFrom(
this.webBrowserInteractionService.extensionInstalled$,
);
const hasDismissedExtensionPrompt = await firstValueFrom(
this.getDialogDismissedState(userId).state$.pipe(map((dismissed) => dismissed ?? false)),
);
if (hasDismissedExtensionPrompt) {
return false;
}
const profileIsWithinThresholds = await this.profileIsWithinThresholds();
if (!profileIsWithinThresholds) {
return false;
}
if (await hasExtensionInstalled) {
return false;
}
const dialogRef = WebVaultExtensionPromptDialogComponent.open(this.dialogService);
await firstValueFrom(dialogRef.closed);
return true;
}
/** Returns the SingleUserState for the dialog dismissed state */
getDialogDismissedState(userId: UserId) {
return this.stateProvider.getUser(userId, WELCOME_EXTENSION_DIALOG_DISMISSED);
}
/**
* Returns true if the user's profile is within the defined thresholds for showing the extension prompt, false otherwise.
* Thresholds are defined as account age between a configurable number of days and 30 days.
*/
private async profileIsWithinThresholds() {
const creationDate = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((account) => account?.creationDate)),
);
// When account or creationDate is not available for some reason,
// default to not showing the prompt to avoid disrupting the user.
if (!creationDate) {
return false;
}
const minAccountAgeDays = await this.configService.getFeatureFlag(
FeatureFlag.PM29438_DialogWithExtensionPromptAccountAge,
);
const now = new Date();
const accountAgeMs = now.getTime() - creationDate.getTime();
const accountAgeDays = accountAgeMs / (1000 * 60 * 60 * 24);
const minAgeDays = minAccountAgeDays ?? 0;
const maxAgeDays = 30;
return accountAgeDays >= minAgeDays && accountAgeDays < maxAgeDays;
}
}

View File

@@ -20,6 +20,7 @@ import {
} from "../../admin-console/organizations/policies";
import { UnifiedUpgradePromptService } from "../../billing/individual/upgrade/services";
import { WebVaultExtensionPromptService } from "./web-vault-extension-prompt.service";
import { WebVaultPromptService } from "./web-vault-prompt.service";
import { WelcomeDialogService } from "./welcome-dialog.service";
@@ -43,6 +44,7 @@ describe("WebVaultPromptService", () => {
const enforceOrganizationDataOwnership = jest.fn().mockResolvedValue(undefined);
const conditionallyShowWelcomeDialog = jest.fn().mockResolvedValue(false);
const logError = jest.fn();
const conditionallyPromptUserForExtension = jest.fn().mockResolvedValue(false);
let activeAccount$: BehaviorSubject<Account | null>;
@@ -74,7 +76,14 @@ describe("WebVaultPromptService", () => {
{ provide: ConfigService, useValue: { getFeatureFlag$ } },
{ provide: DialogService, useValue: { open } },
{ provide: LogService, useValue: { error: logError } },
{ provide: WelcomeDialogService, useValue: { conditionallyShowWelcomeDialog } },
{
provide: WebVaultExtensionPromptService,
useValue: { conditionallyPromptUserForExtension },
},
{
provide: WelcomeDialogService,
useValue: { conditionallyShowWelcomeDialog, conditionallyPromptUserForExtension },
},
],
});
@@ -97,11 +106,19 @@ describe("WebVaultPromptService", () => {
service["vaultItemTransferService"].enforceOrganizationDataOwnership,
).toHaveBeenCalledWith(mockUserId);
});
it("calls conditionallyPromptUserForExtension with the userId", async () => {
await service.conditionallyPromptUser();
expect(
service["webVaultExtensionPromptService"].conditionallyPromptUserForExtension,
).toHaveBeenCalledWith(mockUserId);
});
});
describe("setupAutoConfirm", () => {
it("shows dialog when all conditions are met", fakeAsync(() => {
getFeatureFlag$.mockReturnValueOnce(of(true));
getFeatureFlag$.mockReturnValue(of(true));
configurationAutoConfirm$.mockReturnValueOnce(
of({ showSetupDialog: true, enabled: false, showBrowserNotification: false }),
);

View File

@@ -20,6 +20,7 @@ import {
} from "../../admin-console/organizations/policies";
import { UnifiedUpgradePromptService } from "../../billing/individual/upgrade/services";
import { WebVaultExtensionPromptService } from "./web-vault-extension-prompt.service";
import { WelcomeDialogService } from "./welcome-dialog.service";
@Injectable()
@@ -33,6 +34,7 @@ export class WebVaultPromptService {
private configService = inject(ConfigService);
private dialogService = inject(DialogService);
private logService = inject(LogService);
private webVaultExtensionPromptService = inject(WebVaultExtensionPromptService);
private welcomeDialogService = inject(WelcomeDialogService);
private userId$ = this.accountService.activeAccount$.pipe(getUserId);
@@ -57,6 +59,8 @@ export class WebVaultPromptService {
await this.welcomeDialogService.conditionallyShowWelcomeDialog();
await this.webVaultExtensionPromptService.conditionallyPromptUserForExtension(userId);
this.checkForAutoConfirm();
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

View File

@@ -12877,6 +12877,21 @@
"storageFullDescription": {
"message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage."
},
"extensionPromptHeading": {
"message": "Get the extension for easy vault access"
},
"extensionPromptBody": {
"message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click."
},
"extensionPromptImageAlt": {
"message": "A web browser showing the Bitwarden extension with autofill items for the current webpage."
},
"skip": {
"message": "Skip"
},
"downloadExtension": {
"message": "Download extension"
},
"whoCanView": {
"message": "Who can view"
},

View File

@@ -70,6 +70,8 @@ export enum FeatureFlag {
BrowserPremiumSpotlight = "pm-23384-browser-premium-spotlight",
MigrateMyVaultToMyItems = "pm-20558-migrate-myvault-to-myitems",
PM27632_SdkCipherCrudOperations = "pm-27632-cipher-crud-operations-to-sdk",
PM29438_WelcomeDialogWithExtensionPrompt = "pm-29438-welcome-dialog-with-extension-prompt",
PM29438_DialogWithExtensionPromptAccountAge = "pm-29438-dialog-with-extension-prompt-account-age",
PM29437_WelcomeDialog = "pm-29437-welcome-dialog-no-ext-prompt",
PM31039ItemActionInExtension = "pm-31039-item-action-in-extension",
@@ -137,6 +139,8 @@ export const DefaultFeatureFlagValue = {
[FeatureFlag.BrowserPremiumSpotlight]: FALSE,
[FeatureFlag.PM27632_SdkCipherCrudOperations]: FALSE,
[FeatureFlag.MigrateMyVaultToMyItems]: FALSE,
[FeatureFlag.PM29438_WelcomeDialogWithExtensionPrompt]: FALSE,
[FeatureFlag.PM29438_DialogWithExtensionPromptAccountAge]: 5,
[FeatureFlag.PM29437_WelcomeDialog]: FALSE,
/* Auth */

View File

@@ -220,6 +220,13 @@ export const VAULT_BROWSER_INTRO_CAROUSEL = new StateDefinition(
"disk",
);
export const VAULT_AT_RISK_PASSWORDS_MEMORY = new StateDefinition("vaultAtRiskPasswords", "memory");
export const WELCOME_EXTENSION_DIALOG_DISK = new StateDefinition(
"vaultWelcomeExtensionDialogDismissed",
"disk",
{
web: "disk-local",
},
);
// KM