diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json
index 1df0bf9661..1c3214a6ef 100644
--- a/apps/browser/src/_locales/en/messages.json
+++ b/apps/browser/src/_locales/en/messages.json
@@ -1589,6 +1589,24 @@
"autofillSuggestionsSectionTitle": {
"message": "Autofill suggestions"
},
+ "autofillSpotlightTitle": {
+ "message": "Easily find autofill suggestions"
+ },
+ "autofillSpotlightDesc": {
+ "message": "Turn off your browser's autofill settings, so they don't conflict with Bitwarden."
+ },
+ "turnOffBrowserAutofill": {
+ "message": "Turn off $BROWSER$ autofill",
+ "placeholders": {
+ "browser": {
+ "content": "$1",
+ "example": "Chrome"
+ }
+ }
+ },
+ "turnOffAutofill": {
+ "message": "Turn off autofill"
+ },
"showInlineMenuLabel": {
"message": "Show autofill suggestions on form fields"
},
diff --git a/apps/browser/src/autofill/popup/settings/autofill.component.html b/apps/browser/src/autofill/popup/settings/autofill.component.html
index 4fd85ddce3..264b04b039 100644
--- a/apps/browser/src/autofill/popup/settings/autofill.component.html
+++ b/apps/browser/src/autofill/popup/settings/autofill.component.html
@@ -6,6 +6,16 @@
+
+
+
{{ "autofillSuggestionsSectionTitle" | i18n }}
diff --git a/apps/browser/src/autofill/popup/settings/autofill.component.ts b/apps/browser/src/autofill/popup/settings/autofill.component.ts
index c30f150e71..d63f9a4589 100644
--- a/apps/browser/src/autofill/popup/settings/autofill.component.ts
+++ b/apps/browser/src/autofill/popup/settings/autofill.component.ts
@@ -11,9 +11,11 @@ import {
FormControl,
} from "@angular/forms";
import { RouterModule } from "@angular/router";
-import { firstValueFrom } from "rxjs";
+import { Observable, filter, firstValueFrom, map, switchMap } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
+import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
+import { getUserId } from "@bitwarden/common/auth/services/account.service";
import {
AutofillOverlayVisibility,
BrowserClientVendors,
@@ -53,7 +55,9 @@ import {
SelectModule,
TypographyModule,
} from "@bitwarden/components";
+import { SpotlightComponent, VaultNudgesService, VaultNudgeType } from "@bitwarden/vault";
+import { AutofillBrowserSettingsService } from "../../../autofill/services/autofill-browser-settings.service";
import { BrowserApi } from "../../../platform/browser/browser-api";
import { PopOutComponent } from "../../../platform/popup/components/pop-out.component";
import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component";
@@ -81,6 +85,7 @@ import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.co
SelectModule,
TypographyModule,
ReactiveFormsModule,
+ SpotlightComponent,
],
})
export class AutofillComponent implements OnInit {
@@ -100,6 +105,14 @@ export class AutofillComponent implements OnInit {
protected browserClientIsUnknown: boolean;
protected autofillOnPageLoadFromPolicy$ =
this.autofillSettingsService.activateAutofillOnPageLoadFromPolicy$;
+ protected showSpotlightNudge$: Observable = this.accountService.activeAccount$.pipe(
+ filter((account): account is Account => account !== null),
+ switchMap((account) =>
+ this.vaultNudgesService
+ .showNudge$(VaultNudgeType.AutofillNudge, account.id)
+ .pipe(map((nudgeStatus) => !nudgeStatus.hasSpotlightDismissed)),
+ ),
+ );
protected autofillOnPageLoadForm = new FormGroup({
autofillOnPageLoad: new FormControl(),
@@ -142,6 +155,9 @@ export class AutofillComponent implements OnInit {
private configService: ConfigService,
private formBuilder: FormBuilder,
private destroyRef: DestroyRef,
+ private vaultNudgesService: VaultNudgesService,
+ private accountService: AccountService,
+ private autofillBrowserSettingsService: AutofillBrowserSettingsService,
) {
this.autofillOnPageLoadOptions = [
{ name: this.i18nService.t("autoFillOnPageLoadYes"), value: true },
@@ -165,7 +181,7 @@ export class AutofillComponent implements OnInit {
{ name: i18nService.t("never"), value: UriMatchStrategy.Never },
];
- this.browserClientVendor = this.getBrowserClientVendor();
+ this.browserClientVendor = BrowserApi.getBrowserClientVendor(window);
this.disablePasswordManagerURI = DisablePasswordManagerUris[this.browserClientVendor];
this.browserShortcutsURI = BrowserShortcutsUris[this.browserClientVendor];
this.browserClientIsUnknown = this.browserClientVendor === BrowserClientVendors.Unknown;
@@ -173,7 +189,11 @@ export class AutofillComponent implements OnInit {
async ngOnInit() {
this.canOverrideBrowserAutofillSetting = !this.browserClientIsUnknown;
- this.defaultBrowserAutofillDisabled = await this.browserAutofillSettingCurrentlyOverridden();
+
+ this.defaultBrowserAutofillDisabled =
+ await this.autofillBrowserSettingsService.isBrowserAutofillSettingOverridden(
+ this.browserClientVendor,
+ );
this.inlineMenuVisibility = await firstValueFrom(
this.autofillSettingsService.inlineMenuVisibility$,
@@ -308,6 +328,27 @@ export class AutofillComponent implements OnInit {
);
}
+ get spotlightButtonIcon() {
+ if (this.browserClientVendor === BrowserClientVendors.Unknown) {
+ return "bwi-external-link";
+ }
+ return null;
+ }
+
+ get spotlightButtonText() {
+ if (this.browserClientVendor === BrowserClientVendors.Unknown) {
+ return this.i18nService.t("turnOffAutofill");
+ }
+ return this.i18nService.t("turnOffBrowserAutofill", this.browserClientVendor);
+ }
+
+ async dismissSpotlight() {
+ await this.vaultNudgesService.dismissNudge(
+ VaultNudgeType.AutofillNudge,
+ await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)),
+ );
+ }
+
async updateInlineMenuVisibility() {
if (!this.enableInlineMenu) {
this.enableInlineMenuOnIconSelect = false;
@@ -346,26 +387,6 @@ export class AutofillComponent implements OnInit {
}
}
- private getBrowserClientVendor(): BrowserClientVendor {
- if (this.platformUtilsService.isChrome()) {
- return BrowserClientVendors.Chrome;
- }
-
- if (this.platformUtilsService.isOpera()) {
- return BrowserClientVendors.Opera;
- }
-
- if (this.platformUtilsService.isEdge()) {
- return BrowserClientVendors.Edge;
- }
-
- if (this.platformUtilsService.isVivaldi()) {
- return BrowserClientVendors.Vivaldi;
- }
-
- return BrowserClientVendors.Unknown;
- }
-
protected async openURI(event: Event, uri: BrowserShortcutsUri | DisablePasswordManagerUri) {
event.preventDefault();
@@ -422,7 +443,7 @@ export class AutofillComponent implements OnInit {
if (
this.inlineMenuVisibility === AutofillOverlayVisibility.Off ||
!this.canOverrideBrowserAutofillSetting ||
- (await this.browserAutofillSettingCurrentlyOverridden())
+ this.defaultBrowserAutofillDisabled
) {
return;
}
@@ -460,6 +481,9 @@ export class AutofillComponent implements OnInit {
}
await BrowserApi.updateDefaultBrowserAutofillSettings(!this.defaultBrowserAutofillDisabled);
+ this.autofillBrowserSettingsService.setDefaultBrowserAutofillDisabled(
+ this.defaultBrowserAutofillDisabled,
+ );
}
private handleOverrideDialogAccept = async () => {
@@ -467,18 +491,6 @@ export class AutofillComponent implements OnInit {
await this.updateDefaultBrowserAutofillDisabled();
};
- async browserAutofillSettingCurrentlyOverridden() {
- if (!this.canOverrideBrowserAutofillSetting) {
- return false;
- }
-
- if (!(await this.privacyPermissionGranted())) {
- return false;
- }
-
- return await BrowserApi.browserAutofillSettingsOverridden();
- }
-
async privacyPermissionGranted(): Promise {
return await BrowserApi.permissionsGranted(["privacy"]);
}
diff --git a/apps/browser/src/autofill/services/autofill-browser-settings.service.ts b/apps/browser/src/autofill/services/autofill-browser-settings.service.ts
new file mode 100644
index 0000000000..ba59a655b7
--- /dev/null
+++ b/apps/browser/src/autofill/services/autofill-browser-settings.service.ts
@@ -0,0 +1,31 @@
+import { Injectable } from "@angular/core";
+import { BehaviorSubject, Observable } from "rxjs";
+
+import { BrowserClientVendors } from "@bitwarden/common/autofill/constants";
+import { BrowserClientVendor } from "@bitwarden/common/autofill/types";
+
+import { BrowserApi } from "../../platform/browser/browser-api";
+
+/**
+ * Service class for various Autofill-related browser API operations.
+ */
+@Injectable({
+ providedIn: "root",
+})
+export class AutofillBrowserSettingsService {
+ async isBrowserAutofillSettingOverridden(browserClient: BrowserClientVendor) {
+ return (
+ browserClient !== BrowserClientVendors.Unknown &&
+ (await BrowserApi.browserAutofillSettingsOverridden())
+ );
+ }
+
+ private _defaultBrowserAutofillDisabled$ = new BehaviorSubject(false);
+
+ defaultBrowserAutofillDisabled$: Observable =
+ this._defaultBrowserAutofillDisabled$.asObservable();
+
+ setDefaultBrowserAutofillDisabled(value: boolean) {
+ this._defaultBrowserAutofillDisabled$.next(value);
+ }
+}
diff --git a/apps/browser/src/platform/browser/browser-api.ts b/apps/browser/src/platform/browser/browser-api.ts
index 4b4cec7e7d..b27e8ca7c9 100644
--- a/apps/browser/src/platform/browser/browser-api.ts
+++ b/apps/browser/src/platform/browser/browser-api.ts
@@ -2,6 +2,8 @@
// @ts-strict-ignore
import { Observable } from "rxjs";
+import { BrowserClientVendors } from "@bitwarden/common/autofill/constants";
+import { BrowserClientVendor } from "@bitwarden/common/autofill/types";
import { DeviceType } from "@bitwarden/common/enums";
import { isBrowserSafariApi } from "@bitwarden/platform";
@@ -131,6 +133,27 @@ export class BrowserApi {
});
}
+ static getBrowserClientVendor(clientWindow: Window): BrowserClientVendor {
+ const device = BrowserPlatformUtilsService.getDevice(clientWindow);
+
+ switch (device) {
+ case DeviceType.ChromeExtension:
+ case DeviceType.ChromeBrowser:
+ return BrowserClientVendors.Chrome;
+ case DeviceType.OperaExtension:
+ case DeviceType.OperaBrowser:
+ return BrowserClientVendors.Opera;
+ case DeviceType.EdgeExtension:
+ case DeviceType.EdgeBrowser:
+ return BrowserClientVendors.Edge;
+ case DeviceType.VivaldiExtension:
+ case DeviceType.VivaldiBrowser:
+ return BrowserClientVendors.Vivaldi;
+ default:
+ return BrowserClientVendors.Unknown;
+ }
+ }
+
/**
* Gets the tab with the given id.
*
diff --git a/apps/browser/src/platform/popup/view-cache/popup-view-cache.service.ts b/apps/browser/src/platform/popup/view-cache/popup-view-cache.service.ts
index 6fc3e11493..ff63b52ab3 100644
--- a/apps/browser/src/platform/popup/view-cache/popup-view-cache.service.ts
+++ b/apps/browser/src/platform/popup/view-cache/popup-view-cache.service.ts
@@ -82,7 +82,7 @@ export class PopupViewCacheService implements ViewCacheService {
initialValue,
persistNavigation,
} = options;
- const cachedValue = this.cache[key]
+ const cachedValue = this.cache[key]?.value
? deserializer(JSON.parse(this.cache[key].value))
: initialValue;
const _signal = signal(cachedValue);
diff --git a/apps/browser/src/tools/popup/settings/settings-v2.component.html b/apps/browser/src/tools/popup/settings/settings-v2.component.html
index b9f4176b92..22e2d9a28d 100644
--- a/apps/browser/src/tools/popup/settings/settings-v2.component.html
+++ b/apps/browser/src/tools/popup/settings/settings-v2.component.html
@@ -17,7 +17,15 @@
- {{ "autofill" | i18n }}
+
+
{{ "autofill" | i18n }}
+
1
+
diff --git a/apps/browser/src/tools/popup/settings/settings-v2.component.ts b/apps/browser/src/tools/popup/settings/settings-v2.component.ts
index 9301b8622c..be05452529 100644
--- a/apps/browser/src/tools/popup/settings/settings-v2.component.ts
+++ b/apps/browser/src/tools/popup/settings/settings-v2.component.ts
@@ -1,7 +1,15 @@
import { CommonModule } from "@angular/common";
-import { Component } from "@angular/core";
+import { Component, OnInit } from "@angular/core";
import { RouterModule } from "@angular/router";
-import { filter, firstValueFrom, Observable, shareReplay, switchMap } from "rxjs";
+import {
+ combineLatest,
+ filter,
+ firstValueFrom,
+ map,
+ Observable,
+ shareReplay,
+ switchMap,
+} from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
@@ -12,6 +20,8 @@ import { BadgeComponent, ItemModule } from "@bitwarden/components";
import { NudgeStatus, VaultNudgesService, VaultNudgeType } from "@bitwarden/vault";
import { CurrentAccountComponent } from "../../../auth/popup/account-switching/current-account.component";
+import { AutofillBrowserSettingsService } from "../../../autofill/services/autofill-browser-settings.service";
+import { BrowserApi } from "../../../platform/browser/browser-api";
import { PopOutComponent } from "../../../platform/popup/components/pop-out.component";
import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component";
import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component";
@@ -31,8 +41,10 @@ import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.co
BadgeComponent,
],
})
-export class SettingsV2Component {
+export class SettingsV2Component implements OnInit {
VaultNudgeType = VaultNudgeType;
+ activeUserId: UserId | null = null;
+ protected isBrowserAutofillSettingOverridden = false;
private authenticatedAccount$: Observable = this.accountService.activeAccount$.pipe(
filter((account): account is Account => account !== null),
@@ -51,6 +63,19 @@ export class SettingsV2Component {
),
);
+ showAutofillBadge$: Observable = combineLatest([
+ this.autofillBrowserSettingsService.defaultBrowserAutofillDisabled$,
+ this.authenticatedAccount$,
+ ]).pipe(
+ switchMap(([defaultBrowserAutofillDisabled, account]) =>
+ this.vaultNudgesService.showNudge$(VaultNudgeType.AutofillNudge, account.id).pipe(
+ map((nudgeStatus) => {
+ return !defaultBrowserAutofillDisabled && nudgeStatus.hasBadgeDismissed === false;
+ }),
+ ),
+ ),
+ );
+
protected isNudgeFeatureEnabled$ = this.configService.getFeatureFlag$(
FeatureFlag.PM8851_BrowserOnboardingNudge,
);
@@ -58,9 +83,17 @@ export class SettingsV2Component {
constructor(
private readonly vaultNudgesService: VaultNudgesService,
private readonly accountService: AccountService,
+ private readonly autofillBrowserSettingsService: AutofillBrowserSettingsService,
private readonly configService: ConfigService,
) {}
+ async ngOnInit() {
+ this.isBrowserAutofillSettingOverridden =
+ await this.autofillBrowserSettingsService.isBrowserAutofillSettingOverridden(
+ BrowserApi.getBrowserClientVendor(window),
+ );
+ }
+
async dismissBadge(type: VaultNudgeType) {
if (!(await firstValueFrom(this.showVaultBadge$)).hasBadgeDismissed) {
const account = await firstValueFrom(this.authenticatedAccount$);
diff --git a/libs/vault/src/components/spotlight/spotlight.component.html b/libs/vault/src/components/spotlight/spotlight.component.html
index e949ca4d91..0c6a37914d 100644
--- a/libs/vault/src/components/spotlight/spotlight.component.html
+++ b/libs/vault/src/components/spotlight/spotlight.component.html
@@ -23,10 +23,9 @@
type="button"
buttonType="primary"
*ngIf="buttonText"
- (click)="handleButtonClick()"
+ (click)="handleButtonClick($event)"
>
{{ buttonText }}
+
-
-
diff --git a/libs/vault/src/components/spotlight/spotlight.component.ts b/libs/vault/src/components/spotlight/spotlight.component.ts
index e52669cc40..8639fe7947 100644
--- a/libs/vault/src/components/spotlight/spotlight.component.ts
+++ b/libs/vault/src/components/spotlight/spotlight.component.ts
@@ -19,11 +19,13 @@ export class SpotlightComponent {
@Input() buttonText?: string;
// Wheter the component can be dismissed, if true, the component will not show a close button
@Input() persistent = false;
+ // Optional icon to display on the button
+ @Input() buttonIcon: string | null = null;
@Output() onDismiss = new EventEmitter();
- @Output() onButtonClick = new EventEmitter();
+ @Output() onButtonClick = new EventEmitter();
- handleButtonClick(): void {
- this.onButtonClick.emit();
+ handleButtonClick(event: MouseEvent): void {
+ this.onButtonClick.emit(event);
}
handleDismiss(): void {
diff --git a/libs/vault/src/components/spotlight/spotlight.stories.ts b/libs/vault/src/components/spotlight/spotlight.stories.ts
index 9f7757e452..8e660aacba 100644
--- a/libs/vault/src/components/spotlight/spotlight.stories.ts
+++ b/libs/vault/src/components/spotlight/spotlight.stories.ts
@@ -52,9 +52,9 @@ export const Persistent: Story = {
},
};
-export const WithCustomButton: Story = {
+export const WithButtonIcon: Story = {
args: {
- buttonText: "Custom Button",
+ buttonIcon: "bwi bwi-external-link",
},
render: (args) => ({
props: args,
@@ -62,19 +62,9 @@ export const WithCustomButton: Story = {
-
-
+ buttonText="External Link"
+ buttonIcon="bwi-external-link"
+ >
`,
}),
};
diff --git a/libs/vault/src/services/custom-nudges-services/autofill-nudge.service.ts b/libs/vault/src/services/custom-nudges-services/autofill-nudge.service.ts
new file mode 100644
index 0000000000..b5595e590c
--- /dev/null
+++ b/libs/vault/src/services/custom-nudges-services/autofill-nudge.service.ts
@@ -0,0 +1,47 @@
+import { Injectable, inject } from "@angular/core";
+import { Observable, combineLatest, from, map, of } from "rxjs";
+import { catchError } from "rxjs/operators";
+
+import { VaultProfileService } from "@bitwarden/angular/vault/services/vault-profile.service";
+import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
+import { UserId } from "@bitwarden/common/types/guid";
+
+import { DefaultSingleNudgeService } from "../default-single-nudge.service";
+import { NudgeStatus, VaultNudgeType } from "../vault-nudges.service";
+
+const THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1000;
+
+/**
+ * Custom Nudge Service to use for the Autofill Nudge in the Vault
+ */
+@Injectable({
+ providedIn: "root",
+})
+export class AutofillNudgeService extends DefaultSingleNudgeService {
+ vaultProfileService = inject(VaultProfileService);
+ logService = inject(LogService);
+
+ nudgeStatus$(_: VaultNudgeType, userId: UserId): Observable {
+ const profileDate$ = from(this.vaultProfileService.getProfileCreationDate(userId)).pipe(
+ catchError(() => {
+ this.logService.error("Error getting profile creation date");
+ // Default to today to ensure we show the nudge
+ return of(new Date());
+ }),
+ );
+
+ return combineLatest([
+ profileDate$,
+ this.getNudgeStatus$(VaultNudgeType.AutofillNudge, userId),
+ of(Date.now() - THIRTY_DAYS_MS),
+ ]).pipe(
+ map(([profileCreationDate, status, profileCutoff]) => {
+ const profileOlderThanCutoff = profileCreationDate.getTime() < profileCutoff;
+ return {
+ hasBadgeDismissed: status.hasBadgeDismissed || profileOlderThanCutoff,
+ hasSpotlightDismissed: status.hasSpotlightDismissed || profileOlderThanCutoff,
+ };
+ }),
+ );
+ }
+}
diff --git a/libs/vault/src/services/custom-nudges-services/index.ts b/libs/vault/src/services/custom-nudges-services/index.ts
index 68427a8dc4..2e9ade985c 100644
--- a/libs/vault/src/services/custom-nudges-services/index.ts
+++ b/libs/vault/src/services/custom-nudges-services/index.ts
@@ -1,3 +1,4 @@
+export * from "./autofill-nudge.service";
export * from "./has-items-nudge.service";
export * from "./download-bitwarden-nudge.service";
export * from "./empty-vault-nudge.service";
diff --git a/libs/vault/src/services/vault-nudges.service.spec.ts b/libs/vault/src/services/vault-nudges.service.spec.ts
index a746941071..89465fc538 100644
--- a/libs/vault/src/services/vault-nudges.service.spec.ts
+++ b/libs/vault/src/services/vault-nudges.service.spec.ts
@@ -55,6 +55,7 @@ describe("Vault Nudges Service", () => {
useValue: mock(),
},
{ provide: CipherService, useValue: mock() },
+ { provide: LogService, useValue: mock() },
{
provide: AccountService,
useValue: mock(),
diff --git a/libs/vault/src/services/vault-nudges.service.ts b/libs/vault/src/services/vault-nudges.service.ts
index 171fe85252..e04cb609d7 100644
--- a/libs/vault/src/services/vault-nudges.service.ts
+++ b/libs/vault/src/services/vault-nudges.service.ts
@@ -9,6 +9,7 @@ import { UserId } from "@bitwarden/common/types/guid";
import {
HasItemsNudgeService,
EmptyVaultNudgeService,
+ AutofillNudgeService,
DownloadBitwardenNudgeService,
NewItemNudgeService,
} from "./custom-nudges-services";
@@ -28,6 +29,7 @@ export enum VaultNudgeType {
*/
EmptyVaultNudge = "empty-vault-nudge",
HasVaultItems = "has-vault-items",
+ AutofillNudge = "autofill-nudge",
DownloadBitwarden = "download-bitwarden",
newLoginItemStatus = "new-login-item-status",
newCardItemStatus = "new-card-item-status",
@@ -57,6 +59,7 @@ export class VaultNudgesService {
private customNudgeServices: Partial> = {
[VaultNudgeType.HasVaultItems]: inject(HasItemsNudgeService),
[VaultNudgeType.EmptyVaultNudge]: inject(EmptyVaultNudgeService),
+ [VaultNudgeType.AutofillNudge]: inject(AutofillNudgeService),
[VaultNudgeType.DownloadBitwarden]: inject(DownloadBitwardenNudgeService),
[VaultNudgeType.newLoginItemStatus]: this.newItemNudgeService,
[VaultNudgeType.newCardItemStatus]: this.newItemNudgeService,