1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-15 15:53:27 +00:00

[PM-16804] Add supporting Vault component presentational updates for blocked domains (#12720)

* Revert "remove vault component presentational updates"

This reverts commit fe40dd8464.

* update vault popup autofill service to enable moving state closer to blocked domain component callsites

* hide autofill actions from suggestions if the current tab location is on the blocklist

* update autofill suggestions section title

* update blocked domain section indicator tooltip message

* create and use blocked-injection-banner component

* update blocked URI banner with deeplink to settings

* remove blocked URI indicator for suggestions section

* fix suggested items showing cipher external link button

* fix message catalog updates

* move currentURIIsBlocked state fetching into VaultListItemsContainerComponent

* leverage shareReplay caching for new state additions to VaultPopupAutofillService

* have blocked-injection-banner component consume observable rather than init value

* fix tests

* use observables in the vault-list-items-container template
This commit is contained in:
Jonathan Prusik
2025-01-13 14:43:34 -05:00
committed by GitHub
parent 1fcdf25bf7
commit 3f00f9eaf8
10 changed files with 191 additions and 16 deletions

View File

@@ -2339,11 +2339,11 @@
"blockedDomainsDesc": {
"message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect."
},
"autofillBlockedNotice": {
"message": "Autofill is blocked for this website. Review or change this in settings."
"autofillBlockedNoticeV2": {
"message": "Autofill is blocked for this website."
},
"autofillBlockedTooltip": {
"message": "Autofill is blocked on this website. Review in settings."
"autofillBlockedNoticeGuidance": {
"message": "Change this in settings"
},
"websiteItemLabel": {
"message": "Website $number$ (URI)",
@@ -4007,8 +4007,8 @@
"passkeyRemoved": {
"message": "Passkey removed"
},
"autofillSuggestions": {
"message": "Autofill suggestions"
"itemSuggestions": {
"message": "Suggested items"
},
"autofillSuggestionsTip": {
"message": "Save a login item for this site to autofill"

View File

@@ -1,7 +1,7 @@
<app-vault-list-items-container
*ngIf="autofillCiphers$ | async as ciphers"
[ciphers]="ciphers"
[title]="'autofillSuggestions' | i18n"
[title]="'itemSuggestions' | i18n"
[showRefresh]="showRefresh"
(onRefresh)="refreshCurrentTab()"
[description]="(showEmptyAutofillTip$ | async) ? ('autofillSuggestionsTip' | i18n) : null"

View File

@@ -0,0 +1,10 @@
<bit-banner
*ngIf="showCurrentTabIsBlockedBanner$ | async"
bannerType="info"
(onClose)="handleCurrentTabIsBlockedBannerDismiss()"
>
{{ "autofillBlockedNoticeV2" | i18n }}
<a bitLink linkType="secondary" [routerLink]="blockedURISettingsRoute">
{{ "autofillBlockedNoticeGuidance" | i18n }}
</a>
</bit-banner>

View File

@@ -0,0 +1,53 @@
import { CommonModule } from "@angular/common";
import { Component, OnInit } from "@angular/core";
import { RouterModule } from "@angular/router";
import { Observable } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import {
BannerModule,
IconButtonModule,
LinkModule,
TypographyModule,
} from "@bitwarden/components";
import { VaultPopupAutofillService } from "../../../services/vault-popup-autofill.service";
const blockedURISettingsRoute = "/blocked-domains";
@Component({
standalone: true,
imports: [
BannerModule,
CommonModule,
IconButtonModule,
JslibModule,
LinkModule,
RouterModule,
TypographyModule,
],
selector: "blocked-injection-banner",
templateUrl: "blocked-injection-banner.component.html",
})
export class BlockedInjectionBanner implements OnInit {
/**
* Flag indicating that the banner should be shown
*/
protected showCurrentTabIsBlockedBanner$: Observable<boolean> =
this.vaultPopupAutofillService.showCurrentTabIsBlockedBanner$;
/**
* Hostname for current tab
*/
protected currentTabHostname?: string;
blockedURISettingsRoute: string = blockedURISettingsRoute;
constructor(private vaultPopupAutofillService: VaultPopupAutofillService) {}
async ngOnInit() {}
async handleCurrentTabIsBlockedBannerDismiss() {
await this.vaultPopupAutofillService.dismissCurrentTabIsBlockedBanner();
}
}

View File

@@ -80,11 +80,9 @@
<button
bit-item-content
type="button"
(click)="primaryActionAutofill ? doAutofill(cipher) : onViewCipher(cipher)"
(click)="primaryActionOnSelect(cipher)"
(dblclick)="launchCipher(cipher)"
[appA11yTitle]="
(primaryActionAutofill ? 'autofillTitle' : 'viewItemTitle') | i18n: cipher.name
"
[appA11yTitle]="cipherItemTitleKey | async | i18n: cipher.name"
class="{{ itemHeightClass }}"
>
<div slot="start" class="tw-justify-start tw-w-7 tw-flex">
@@ -106,7 +104,7 @@
<span slot="secondary">{{ cipher.subTitle }}</span>
</button>
<ng-container slot="end">
<bit-item-action *ngIf="showAutofillButton && !primaryActionAutofill">
<bit-item-action *ngIf="!(hideAutofillButton$ | async)">
<button
type="button"
bitBadge
@@ -131,7 +129,7 @@
<app-item-copy-actions [cipher]="cipher"></app-item-copy-actions>
<app-item-more-options
[cipher]="cipher"
[hideAutofillOptions]="showAutofillButton"
[hideAutofillOptions]="hideAutofillOptions$ | async"
[showViewOption]="primaryActionAutofill"
></app-item-more-options>
</ng-container>

View File

@@ -15,8 +15,8 @@ import {
signal,
ViewChild,
} from "@angular/core";
import { Router, RouterLink } from "@angular/router";
import { map } from "rxjs";
import { Router } from "@angular/router";
import { firstValueFrom, Observable, map } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@@ -62,7 +62,6 @@ import { ItemMoreOptionsComponent } from "../item-more-options/item-more-options
TypographyModule,
JslibModule,
SectionHeaderComponent,
RouterLink,
ItemCopyActionsComponent,
ItemMoreOptionsComponent,
OrgIconDirective,
@@ -151,12 +150,41 @@ export class VaultListItemsContainerComponent implements OnInit, AfterViewInit {
@Output()
onRefresh = new EventEmitter<void>();
/**
* Flag indicating that the current tab location is blocked
*/
currentURIIsBlocked$: Observable<boolean> =
this.vaultPopupAutofillService.currentTabIsOnBlocklist$;
/**
* Resolved i18n key to use for suggested cipher items
*/
cipherItemTitleKey = this.currentURIIsBlocked$.pipe(
map((uriIsBlocked) =>
this.primaryActionAutofill && !uriIsBlocked ? "autofillTitle" : "viewItemTitle",
),
);
/**
* Option to show the autofill button for each item.
*/
@Input({ transform: booleanAttribute })
showAutofillButton: boolean;
/**
* Flag indicating whether the suggested cipher item autofill button should be shown or not
*/
hideAutofillButton$ = this.currentURIIsBlocked$.pipe(
map((uriIsBlocked) => !this.showAutofillButton || uriIsBlocked || this.primaryActionAutofill),
);
/**
* Flag indicating whether the cipher item autofill options should be shown or not
*/
hideAutofillOptions$: Observable<boolean> = this.currentURIIsBlocked$.pipe(
map((uriIsBlocked) => uriIsBlocked || this.showAutofillButton),
);
/**
* Option to perform autofill operation as the primary action for autofill suggestions.
*/
@@ -216,6 +244,14 @@ export class VaultListItemsContainerComponent implements OnInit, AfterViewInit {
}
}
async primaryActionOnSelect(cipher: CipherView) {
const isBlocked = await firstValueFrom(this.currentURIIsBlocked$);
return this.primaryActionAutofill && !isBlocked
? this.doAutofill(cipher)
: this.onViewCipher(cipher);
}
/**
* Launches the login cipher in a new browser tab.
*/

View File

@@ -22,6 +22,11 @@
</bit-no-items>
</div>
<blocked-injection-banner
*ngIf="vaultState !== VaultStateEnum.Empty"
slot="full-width-notice"
></blocked-injection-banner>
<!-- Show search & filters outside of the scroll area of the page -->
<ng-container
slot="above-scroll-area"

View File

@@ -21,6 +21,7 @@ import { VaultPopupItemsService } from "../../services/vault-popup-items.service
import { VaultPopupListFiltersService } from "../../services/vault-popup-list-filters.service";
import { VaultUiOnboardingService } from "../../services/vault-ui-onboarding.service";
import { BlockedInjectionBanner } from "./blocked-injection-banner/blocked-injection-banner.component";
import {
NewItemDropdownV2Component,
NewItemInitialValues,
@@ -40,6 +41,7 @@ enum VaultState {
templateUrl: "vault-v2.component.html",
standalone: true,
imports: [
BlockedInjectionBanner,
PopupPageComponent,
PopupHeaderComponent,
PopOutComponent,

View File

@@ -4,6 +4,7 @@ import { mock } from "jest-mock-extended";
import { BehaviorSubject, of } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
@@ -44,6 +45,7 @@ describe("VaultPopupAutofillService", () => {
// Create mocks for VaultPopupAutofillService
const mockAutofillService = mock<AutofillService>();
const mockDomainSettingsService = mock<DomainSettingsService>();
const mockI18nService = mock<I18nService>();
const mockToastService = mock<ToastService>();
const mockPlatformUtilsService = mock<PlatformUtilsService>();
@@ -71,6 +73,7 @@ describe("VaultPopupAutofillService", () => {
testBed = TestBed.configureTestingModule({
providers: [
{ provide: AutofillService, useValue: mockAutofillService },
{ provide: DomainSettingsService, useValue: mockDomainSettingsService },
{ provide: I18nService, useValue: mockI18nService },
{ provide: ToastService, useValue: mockToastService },
{ provide: PlatformUtilsService, useValue: mockPlatformUtilsService },

View File

@@ -15,6 +15,7 @@ import {
} from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
@@ -67,6 +68,72 @@ export class VaultPopupAutofillService {
shareReplay({ refCount: false, bufferSize: 1 }),
);
currentTabIsOnBlocklist$: Observable<boolean> = combineLatest([
this.domainSettingsService.blockedInteractionsUris$,
this.currentAutofillTab$,
]).pipe(
map(([blockedInteractionsUris, currentTab]) => {
if (blockedInteractionsUris && currentTab?.url?.length) {
const tabURL = new URL(currentTab.url);
const tabIsBlocked = Object.keys(blockedInteractionsUris).includes(tabURL.hostname);
if (tabIsBlocked) {
return true;
}
}
return false;
}),
shareReplay({ refCount: false, bufferSize: 1 }),
);
showCurrentTabIsBlockedBanner$: Observable<boolean> = combineLatest([
this.domainSettingsService.blockedInteractionsUris$,
this.currentAutofillTab$,
]).pipe(
map(([blockedInteractionsUris, currentTab]) => {
if (blockedInteractionsUris && currentTab?.url?.length) {
const tabURL = new URL(currentTab.url);
const tabIsBlocked = Object.keys(blockedInteractionsUris).includes(tabURL.hostname);
const showScriptInjectionIsBlockedBanner =
tabIsBlocked && !blockedInteractionsUris[tabURL.hostname]?.bannerIsDismissed;
return showScriptInjectionIsBlockedBanner;
}
return false;
}),
shareReplay({ refCount: false, bufferSize: 1 }),
);
async dismissCurrentTabIsBlockedBanner() {
try {
const currentTab = await firstValueFrom(this.currentAutofillTab$);
const currentTabURL = currentTab?.url.length && new URL(currentTab.url);
const currentTabHostname = currentTabURL && currentTabURL.hostname;
if (!currentTabHostname) {
return;
}
const blockedURIs = await firstValueFrom(this.domainSettingsService.blockedInteractionsUris$);
const tabIsBlocked = Object.keys(blockedURIs).includes(currentTabHostname);
if (tabIsBlocked) {
void this.domainSettingsService.setBlockedInteractionsUris({
...blockedURIs,
[currentTabHostname as string]: { bannerIsDismissed: true },
});
}
} catch (e) {
throw new Error(
"There was a problem dismissing the blocked interaction URI notification banner",
);
}
}
/**
* Observable that indicates whether autofill is allowed in the current context.
* Autofill is allowed when there is a current tab and the popup is not in a popout window.
@@ -125,6 +192,7 @@ export class VaultPopupAutofillService {
constructor(
private autofillService: AutofillService,
private domainSettingsService: DomainSettingsService,
private i18nService: I18nService,
private toastService: ToastService,
private platformUtilService: PlatformUtilsService,