mirror of
https://github.com/bitwarden/browser
synced 2025-12-06 00:13:28 +00:00
[PM-18802] - Autofill Settings Nudges and Settings Badge (#14439)
* autofill nudge * remove undismiss logic * revert change to popup view cache service * move browser autofill logic to platform. cleanup * fix test * adjustments to autofill nudges * add missing provider * updates to autofill nudges * fix date logic * change autofillBrowserSettingsService isBrowserAutofillSettingOverridden to function * fix up browser autofill overridden settings logic * remove check for privacy in isBrowserAutofillSettingOverridden
This commit is contained in:
@@ -1589,6 +1589,24 @@
|
|||||||
"autofillSuggestionsSectionTitle": {
|
"autofillSuggestionsSectionTitle": {
|
||||||
"message": "Autofill suggestions"
|
"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": {
|
"showInlineMenuLabel": {
|
||||||
"message": "Show autofill suggestions on form fields"
|
"message": "Show autofill suggestions on form fields"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6,6 +6,16 @@
|
|||||||
</popup-header>
|
</popup-header>
|
||||||
|
|
||||||
<div class="tw-bg-background-alt">
|
<div class="tw-bg-background-alt">
|
||||||
|
<div *ngIf="!defaultBrowserAutofillDisabled && (showSpotlightNudge$ | async)" class="tw-mb-6">
|
||||||
|
<bit-spotlight
|
||||||
|
[title]="'autofillSpotlightTitle' | i18n"
|
||||||
|
[subtitle]="'autofillSpotlightDesc' | i18n"
|
||||||
|
[buttonText]="spotlightButtonText"
|
||||||
|
(onDismiss)="dismissSpotlight()"
|
||||||
|
(onButtonClick)="openURI($event, disablePasswordManagerURI)"
|
||||||
|
[buttonIcon]="spotlightButtonIcon"
|
||||||
|
></bit-spotlight>
|
||||||
|
</div>
|
||||||
<bit-section>
|
<bit-section>
|
||||||
<bit-section-header>
|
<bit-section-header>
|
||||||
<h2 bitTypography="h6">{{ "autofillSuggestionsSectionTitle" | i18n }}</h2>
|
<h2 bitTypography="h6">{{ "autofillSuggestionsSectionTitle" | i18n }}</h2>
|
||||||
|
|||||||
@@ -11,9 +11,11 @@ import {
|
|||||||
FormControl,
|
FormControl,
|
||||||
} from "@angular/forms";
|
} from "@angular/forms";
|
||||||
import { RouterModule } from "@angular/router";
|
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 { 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 {
|
import {
|
||||||
AutofillOverlayVisibility,
|
AutofillOverlayVisibility,
|
||||||
BrowserClientVendors,
|
BrowserClientVendors,
|
||||||
@@ -53,7 +55,9 @@ import {
|
|||||||
SelectModule,
|
SelectModule,
|
||||||
TypographyModule,
|
TypographyModule,
|
||||||
} from "@bitwarden/components";
|
} 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 { BrowserApi } from "../../../platform/browser/browser-api";
|
||||||
import { PopOutComponent } from "../../../platform/popup/components/pop-out.component";
|
import { PopOutComponent } from "../../../platform/popup/components/pop-out.component";
|
||||||
import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component";
|
import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component";
|
||||||
@@ -81,6 +85,7 @@ import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.co
|
|||||||
SelectModule,
|
SelectModule,
|
||||||
TypographyModule,
|
TypographyModule,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
|
SpotlightComponent,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class AutofillComponent implements OnInit {
|
export class AutofillComponent implements OnInit {
|
||||||
@@ -100,6 +105,14 @@ export class AutofillComponent implements OnInit {
|
|||||||
protected browserClientIsUnknown: boolean;
|
protected browserClientIsUnknown: boolean;
|
||||||
protected autofillOnPageLoadFromPolicy$ =
|
protected autofillOnPageLoadFromPolicy$ =
|
||||||
this.autofillSettingsService.activateAutofillOnPageLoadFromPolicy$;
|
this.autofillSettingsService.activateAutofillOnPageLoadFromPolicy$;
|
||||||
|
protected showSpotlightNudge$: Observable<boolean> = 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({
|
protected autofillOnPageLoadForm = new FormGroup({
|
||||||
autofillOnPageLoad: new FormControl(),
|
autofillOnPageLoad: new FormControl(),
|
||||||
@@ -142,6 +155,9 @@ export class AutofillComponent implements OnInit {
|
|||||||
private configService: ConfigService,
|
private configService: ConfigService,
|
||||||
private formBuilder: FormBuilder,
|
private formBuilder: FormBuilder,
|
||||||
private destroyRef: DestroyRef,
|
private destroyRef: DestroyRef,
|
||||||
|
private vaultNudgesService: VaultNudgesService,
|
||||||
|
private accountService: AccountService,
|
||||||
|
private autofillBrowserSettingsService: AutofillBrowserSettingsService,
|
||||||
) {
|
) {
|
||||||
this.autofillOnPageLoadOptions = [
|
this.autofillOnPageLoadOptions = [
|
||||||
{ name: this.i18nService.t("autoFillOnPageLoadYes"), value: true },
|
{ name: this.i18nService.t("autoFillOnPageLoadYes"), value: true },
|
||||||
@@ -165,7 +181,7 @@ export class AutofillComponent implements OnInit {
|
|||||||
{ name: i18nService.t("never"), value: UriMatchStrategy.Never },
|
{ name: i18nService.t("never"), value: UriMatchStrategy.Never },
|
||||||
];
|
];
|
||||||
|
|
||||||
this.browserClientVendor = this.getBrowserClientVendor();
|
this.browserClientVendor = BrowserApi.getBrowserClientVendor(window);
|
||||||
this.disablePasswordManagerURI = DisablePasswordManagerUris[this.browserClientVendor];
|
this.disablePasswordManagerURI = DisablePasswordManagerUris[this.browserClientVendor];
|
||||||
this.browserShortcutsURI = BrowserShortcutsUris[this.browserClientVendor];
|
this.browserShortcutsURI = BrowserShortcutsUris[this.browserClientVendor];
|
||||||
this.browserClientIsUnknown = this.browserClientVendor === BrowserClientVendors.Unknown;
|
this.browserClientIsUnknown = this.browserClientVendor === BrowserClientVendors.Unknown;
|
||||||
@@ -173,7 +189,11 @@ export class AutofillComponent implements OnInit {
|
|||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
this.canOverrideBrowserAutofillSetting = !this.browserClientIsUnknown;
|
this.canOverrideBrowserAutofillSetting = !this.browserClientIsUnknown;
|
||||||
this.defaultBrowserAutofillDisabled = await this.browserAutofillSettingCurrentlyOverridden();
|
|
||||||
|
this.defaultBrowserAutofillDisabled =
|
||||||
|
await this.autofillBrowserSettingsService.isBrowserAutofillSettingOverridden(
|
||||||
|
this.browserClientVendor,
|
||||||
|
);
|
||||||
|
|
||||||
this.inlineMenuVisibility = await firstValueFrom(
|
this.inlineMenuVisibility = await firstValueFrom(
|
||||||
this.autofillSettingsService.inlineMenuVisibility$,
|
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() {
|
async updateInlineMenuVisibility() {
|
||||||
if (!this.enableInlineMenu) {
|
if (!this.enableInlineMenu) {
|
||||||
this.enableInlineMenuOnIconSelect = false;
|
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) {
|
protected async openURI(event: Event, uri: BrowserShortcutsUri | DisablePasswordManagerUri) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
@@ -422,7 +443,7 @@ export class AutofillComponent implements OnInit {
|
|||||||
if (
|
if (
|
||||||
this.inlineMenuVisibility === AutofillOverlayVisibility.Off ||
|
this.inlineMenuVisibility === AutofillOverlayVisibility.Off ||
|
||||||
!this.canOverrideBrowserAutofillSetting ||
|
!this.canOverrideBrowserAutofillSetting ||
|
||||||
(await this.browserAutofillSettingCurrentlyOverridden())
|
this.defaultBrowserAutofillDisabled
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -460,6 +481,9 @@ export class AutofillComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await BrowserApi.updateDefaultBrowserAutofillSettings(!this.defaultBrowserAutofillDisabled);
|
await BrowserApi.updateDefaultBrowserAutofillSettings(!this.defaultBrowserAutofillDisabled);
|
||||||
|
this.autofillBrowserSettingsService.setDefaultBrowserAutofillDisabled(
|
||||||
|
this.defaultBrowserAutofillDisabled,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleOverrideDialogAccept = async () => {
|
private handleOverrideDialogAccept = async () => {
|
||||||
@@ -467,18 +491,6 @@ export class AutofillComponent implements OnInit {
|
|||||||
await this.updateDefaultBrowserAutofillDisabled();
|
await this.updateDefaultBrowserAutofillDisabled();
|
||||||
};
|
};
|
||||||
|
|
||||||
async browserAutofillSettingCurrentlyOverridden() {
|
|
||||||
if (!this.canOverrideBrowserAutofillSetting) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!(await this.privacyPermissionGranted())) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return await BrowserApi.browserAutofillSettingsOverridden();
|
|
||||||
}
|
|
||||||
|
|
||||||
async privacyPermissionGranted(): Promise<boolean> {
|
async privacyPermissionGranted(): Promise<boolean> {
|
||||||
return await BrowserApi.permissionsGranted(["privacy"]);
|
return await BrowserApi.permissionsGranted(["privacy"]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<boolean>(false);
|
||||||
|
|
||||||
|
defaultBrowserAutofillDisabled$: Observable<boolean> =
|
||||||
|
this._defaultBrowserAutofillDisabled$.asObservable();
|
||||||
|
|
||||||
|
setDefaultBrowserAutofillDisabled(value: boolean) {
|
||||||
|
this._defaultBrowserAutofillDisabled$.next(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,8 @@
|
|||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
import { Observable } from "rxjs";
|
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 { DeviceType } from "@bitwarden/common/enums";
|
||||||
import { isBrowserSafariApi } from "@bitwarden/platform";
|
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.
|
* Gets the tab with the given id.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ export class PopupViewCacheService implements ViewCacheService {
|
|||||||
initialValue,
|
initialValue,
|
||||||
persistNavigation,
|
persistNavigation,
|
||||||
} = options;
|
} = options;
|
||||||
const cachedValue = this.cache[key]
|
const cachedValue = this.cache[key]?.value
|
||||||
? deserializer(JSON.parse(this.cache[key].value))
|
? deserializer(JSON.parse(this.cache[key].value))
|
||||||
: initialValue;
|
: initialValue;
|
||||||
const _signal = signal(cachedValue);
|
const _signal = signal(cachedValue);
|
||||||
|
|||||||
@@ -17,7 +17,15 @@
|
|||||||
<bit-item>
|
<bit-item>
|
||||||
<a bit-item-content routerLink="/autofill">
|
<a bit-item-content routerLink="/autofill">
|
||||||
<i slot="start" class="bwi bwi-check-circle" aria-hidden="true"></i>
|
<i slot="start" class="bwi bwi-check-circle" aria-hidden="true"></i>
|
||||||
{{ "autofill" | i18n }}
|
<div class="tw-flex tw-items-center tw-justify-center">
|
||||||
|
<p class="tw-pr-2">{{ "autofill" | i18n }}</p>
|
||||||
|
<span
|
||||||
|
*ngIf="!isBrowserAutofillSettingOverridden && (showAutofillBadge$ | async)"
|
||||||
|
bitBadge
|
||||||
|
variant="notification"
|
||||||
|
>1</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
|
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
</bit-item>
|
</bit-item>
|
||||||
|
|||||||
@@ -1,7 +1,15 @@
|
|||||||
import { CommonModule } from "@angular/common";
|
import { CommonModule } from "@angular/common";
|
||||||
import { Component } from "@angular/core";
|
import { Component, OnInit } from "@angular/core";
|
||||||
import { RouterModule } from "@angular/router";
|
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 { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
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 { NudgeStatus, VaultNudgesService, VaultNudgeType } from "@bitwarden/vault";
|
||||||
|
|
||||||
import { CurrentAccountComponent } from "../../../auth/popup/account-switching/current-account.component";
|
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 { PopOutComponent } from "../../../platform/popup/components/pop-out.component";
|
||||||
import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component";
|
import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component";
|
||||||
import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component";
|
import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component";
|
||||||
@@ -31,8 +41,10 @@ import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.co
|
|||||||
BadgeComponent,
|
BadgeComponent,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class SettingsV2Component {
|
export class SettingsV2Component implements OnInit {
|
||||||
VaultNudgeType = VaultNudgeType;
|
VaultNudgeType = VaultNudgeType;
|
||||||
|
activeUserId: UserId | null = null;
|
||||||
|
protected isBrowserAutofillSettingOverridden = false;
|
||||||
|
|
||||||
private authenticatedAccount$: Observable<Account> = this.accountService.activeAccount$.pipe(
|
private authenticatedAccount$: Observable<Account> = this.accountService.activeAccount$.pipe(
|
||||||
filter((account): account is Account => account !== null),
|
filter((account): account is Account => account !== null),
|
||||||
@@ -51,6 +63,19 @@ export class SettingsV2Component {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
showAutofillBadge$: Observable<boolean> = 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$(
|
protected isNudgeFeatureEnabled$ = this.configService.getFeatureFlag$(
|
||||||
FeatureFlag.PM8851_BrowserOnboardingNudge,
|
FeatureFlag.PM8851_BrowserOnboardingNudge,
|
||||||
);
|
);
|
||||||
@@ -58,9 +83,17 @@ export class SettingsV2Component {
|
|||||||
constructor(
|
constructor(
|
||||||
private readonly vaultNudgesService: VaultNudgesService,
|
private readonly vaultNudgesService: VaultNudgesService,
|
||||||
private readonly accountService: AccountService,
|
private readonly accountService: AccountService,
|
||||||
|
private readonly autofillBrowserSettingsService: AutofillBrowserSettingsService,
|
||||||
private readonly configService: ConfigService,
|
private readonly configService: ConfigService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
async ngOnInit() {
|
||||||
|
this.isBrowserAutofillSettingOverridden =
|
||||||
|
await this.autofillBrowserSettingsService.isBrowserAutofillSettingOverridden(
|
||||||
|
BrowserApi.getBrowserClientVendor(window),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async dismissBadge(type: VaultNudgeType) {
|
async dismissBadge(type: VaultNudgeType) {
|
||||||
if (!(await firstValueFrom(this.showVaultBadge$)).hasBadgeDismissed) {
|
if (!(await firstValueFrom(this.showVaultBadge$)).hasBadgeDismissed) {
|
||||||
const account = await firstValueFrom(this.authenticatedAccount$);
|
const account = await firstValueFrom(this.authenticatedAccount$);
|
||||||
|
|||||||
@@ -23,10 +23,9 @@
|
|||||||
type="button"
|
type="button"
|
||||||
buttonType="primary"
|
buttonType="primary"
|
||||||
*ngIf="buttonText"
|
*ngIf="buttonText"
|
||||||
(click)="handleButtonClick()"
|
(click)="handleButtonClick($event)"
|
||||||
>
|
>
|
||||||
{{ buttonText }}
|
{{ buttonText }}
|
||||||
|
<i *ngIf="buttonIcon" [ngClass]="buttonIcon" class="bwi tw-ml-1" aria-hidden="true"></i>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<ng-content></ng-content>
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -19,11 +19,13 @@ export class SpotlightComponent {
|
|||||||
@Input() buttonText?: string;
|
@Input() buttonText?: string;
|
||||||
// Wheter the component can be dismissed, if true, the component will not show a close button
|
// Wheter the component can be dismissed, if true, the component will not show a close button
|
||||||
@Input() persistent = false;
|
@Input() persistent = false;
|
||||||
|
// Optional icon to display on the button
|
||||||
|
@Input() buttonIcon: string | null = null;
|
||||||
@Output() onDismiss = new EventEmitter<void>();
|
@Output() onDismiss = new EventEmitter<void>();
|
||||||
@Output() onButtonClick = new EventEmitter<void>();
|
@Output() onButtonClick = new EventEmitter();
|
||||||
|
|
||||||
handleButtonClick(): void {
|
handleButtonClick(event: MouseEvent): void {
|
||||||
this.onButtonClick.emit();
|
this.onButtonClick.emit(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDismiss(): void {
|
handleDismiss(): void {
|
||||||
|
|||||||
@@ -52,9 +52,9 @@ export const Persistent: Story = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const WithCustomButton: Story = {
|
export const WithButtonIcon: Story = {
|
||||||
args: {
|
args: {
|
||||||
buttonText: "Custom Button",
|
buttonIcon: "bwi bwi-external-link",
|
||||||
},
|
},
|
||||||
render: (args) => ({
|
render: (args) => ({
|
||||||
props: args,
|
props: args,
|
||||||
@@ -62,19 +62,9 @@ export const WithCustomButton: Story = {
|
|||||||
<bit-spotlight
|
<bit-spotlight
|
||||||
[title]="title"
|
[title]="title"
|
||||||
[subtitle]="subtitle"
|
[subtitle]="subtitle"
|
||||||
>
|
buttonText="External Link"
|
||||||
<button
|
buttonIcon="bwi-external-link"
|
||||||
class="tw-w-full"
|
></bit-spotlight>
|
||||||
bit-item-content
|
|
||||||
bitButton
|
|
||||||
type="button"
|
|
||||||
buttonType="primary"
|
|
||||||
(click)="handleButtonClick()"
|
|
||||||
>
|
|
||||||
External Link
|
|
||||||
<i slot="end" class="bwi bwi-external-link ml-2" aria-hidden="true"></i>
|
|
||||||
</button>
|
|
||||||
</bit-spotlight>
|
|
||||||
`,
|
`,
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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<NudgeStatus> {
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
export * from "./autofill-nudge.service";
|
||||||
export * from "./has-items-nudge.service";
|
export * from "./has-items-nudge.service";
|
||||||
export * from "./download-bitwarden-nudge.service";
|
export * from "./download-bitwarden-nudge.service";
|
||||||
export * from "./empty-vault-nudge.service";
|
export * from "./empty-vault-nudge.service";
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ describe("Vault Nudges Service", () => {
|
|||||||
useValue: mock<ApiService>(),
|
useValue: mock<ApiService>(),
|
||||||
},
|
},
|
||||||
{ provide: CipherService, useValue: mock<CipherService>() },
|
{ provide: CipherService, useValue: mock<CipherService>() },
|
||||||
|
{ provide: LogService, useValue: mock<LogService>() },
|
||||||
{
|
{
|
||||||
provide: AccountService,
|
provide: AccountService,
|
||||||
useValue: mock<AccountService>(),
|
useValue: mock<AccountService>(),
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { UserId } from "@bitwarden/common/types/guid";
|
|||||||
import {
|
import {
|
||||||
HasItemsNudgeService,
|
HasItemsNudgeService,
|
||||||
EmptyVaultNudgeService,
|
EmptyVaultNudgeService,
|
||||||
|
AutofillNudgeService,
|
||||||
DownloadBitwardenNudgeService,
|
DownloadBitwardenNudgeService,
|
||||||
NewItemNudgeService,
|
NewItemNudgeService,
|
||||||
} from "./custom-nudges-services";
|
} from "./custom-nudges-services";
|
||||||
@@ -28,6 +29,7 @@ export enum VaultNudgeType {
|
|||||||
*/
|
*/
|
||||||
EmptyVaultNudge = "empty-vault-nudge",
|
EmptyVaultNudge = "empty-vault-nudge",
|
||||||
HasVaultItems = "has-vault-items",
|
HasVaultItems = "has-vault-items",
|
||||||
|
AutofillNudge = "autofill-nudge",
|
||||||
DownloadBitwarden = "download-bitwarden",
|
DownloadBitwarden = "download-bitwarden",
|
||||||
newLoginItemStatus = "new-login-item-status",
|
newLoginItemStatus = "new-login-item-status",
|
||||||
newCardItemStatus = "new-card-item-status",
|
newCardItemStatus = "new-card-item-status",
|
||||||
@@ -57,6 +59,7 @@ export class VaultNudgesService {
|
|||||||
private customNudgeServices: Partial<Record<VaultNudgeType, SingleNudgeService>> = {
|
private customNudgeServices: Partial<Record<VaultNudgeType, SingleNudgeService>> = {
|
||||||
[VaultNudgeType.HasVaultItems]: inject(HasItemsNudgeService),
|
[VaultNudgeType.HasVaultItems]: inject(HasItemsNudgeService),
|
||||||
[VaultNudgeType.EmptyVaultNudge]: inject(EmptyVaultNudgeService),
|
[VaultNudgeType.EmptyVaultNudge]: inject(EmptyVaultNudgeService),
|
||||||
|
[VaultNudgeType.AutofillNudge]: inject(AutofillNudgeService),
|
||||||
[VaultNudgeType.DownloadBitwarden]: inject(DownloadBitwardenNudgeService),
|
[VaultNudgeType.DownloadBitwarden]: inject(DownloadBitwardenNudgeService),
|
||||||
[VaultNudgeType.newLoginItemStatus]: this.newItemNudgeService,
|
[VaultNudgeType.newLoginItemStatus]: this.newItemNudgeService,
|
||||||
[VaultNudgeType.newCardItemStatus]: this.newItemNudgeService,
|
[VaultNudgeType.newCardItemStatus]: this.newItemNudgeService,
|
||||||
|
|||||||
Reference in New Issue
Block a user