1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-16 16:23:44 +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": { "blockedDomainsDesc": {
"message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect."
}, },
"autofillBlockedNotice": { "autofillBlockedNoticeV2": {
"message": "Autofill is blocked for this website. Review or change this in settings." "message": "Autofill is blocked for this website."
}, },
"autofillBlockedTooltip": { "autofillBlockedNoticeGuidance": {
"message": "Autofill is blocked on this website. Review in settings." "message": "Change this in settings"
}, },
"websiteItemLabel": { "websiteItemLabel": {
"message": "Website $number$ (URI)", "message": "Website $number$ (URI)",
@@ -4007,8 +4007,8 @@
"passkeyRemoved": { "passkeyRemoved": {
"message": "Passkey removed" "message": "Passkey removed"
}, },
"autofillSuggestions": { "itemSuggestions": {
"message": "Autofill suggestions" "message": "Suggested items"
}, },
"autofillSuggestionsTip": { "autofillSuggestionsTip": {
"message": "Save a login item for this site to autofill" "message": "Save a login item for this site to autofill"

View File

@@ -1,7 +1,7 @@
<app-vault-list-items-container <app-vault-list-items-container
*ngIf="autofillCiphers$ | async as ciphers" *ngIf="autofillCiphers$ | async as ciphers"
[ciphers]="ciphers" [ciphers]="ciphers"
[title]="'autofillSuggestions' | i18n" [title]="'itemSuggestions' | i18n"
[showRefresh]="showRefresh" [showRefresh]="showRefresh"
(onRefresh)="refreshCurrentTab()" (onRefresh)="refreshCurrentTab()"
[description]="(showEmptyAutofillTip$ | async) ? ('autofillSuggestionsTip' | i18n) : null" [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 <button
bit-item-content bit-item-content
type="button" type="button"
(click)="primaryActionAutofill ? doAutofill(cipher) : onViewCipher(cipher)" (click)="primaryActionOnSelect(cipher)"
(dblclick)="launchCipher(cipher)" (dblclick)="launchCipher(cipher)"
[appA11yTitle]=" [appA11yTitle]="cipherItemTitleKey | async | i18n: cipher.name"
(primaryActionAutofill ? 'autofillTitle' : 'viewItemTitle') | i18n: cipher.name
"
class="{{ itemHeightClass }}" class="{{ itemHeightClass }}"
> >
<div slot="start" class="tw-justify-start tw-w-7 tw-flex"> <div slot="start" class="tw-justify-start tw-w-7 tw-flex">
@@ -106,7 +104,7 @@
<span slot="secondary">{{ cipher.subTitle }}</span> <span slot="secondary">{{ cipher.subTitle }}</span>
</button> </button>
<ng-container slot="end"> <ng-container slot="end">
<bit-item-action *ngIf="showAutofillButton && !primaryActionAutofill"> <bit-item-action *ngIf="!(hideAutofillButton$ | async)">
<button <button
type="button" type="button"
bitBadge bitBadge
@@ -131,7 +129,7 @@
<app-item-copy-actions [cipher]="cipher"></app-item-copy-actions> <app-item-copy-actions [cipher]="cipher"></app-item-copy-actions>
<app-item-more-options <app-item-more-options
[cipher]="cipher" [cipher]="cipher"
[hideAutofillOptions]="showAutofillButton" [hideAutofillOptions]="hideAutofillOptions$ | async"
[showViewOption]="primaryActionAutofill" [showViewOption]="primaryActionAutofill"
></app-item-more-options> ></app-item-more-options>
</ng-container> </ng-container>

View File

@@ -15,8 +15,8 @@ import {
signal, signal,
ViewChild, ViewChild,
} from "@angular/core"; } from "@angular/core";
import { Router, RouterLink } from "@angular/router"; import { Router } from "@angular/router";
import { map } from "rxjs"; import { firstValueFrom, Observable, map } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module"; import { JslibModule } from "@bitwarden/angular/jslib.module";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@@ -62,7 +62,6 @@ import { ItemMoreOptionsComponent } from "../item-more-options/item-more-options
TypographyModule, TypographyModule,
JslibModule, JslibModule,
SectionHeaderComponent, SectionHeaderComponent,
RouterLink,
ItemCopyActionsComponent, ItemCopyActionsComponent,
ItemMoreOptionsComponent, ItemMoreOptionsComponent,
OrgIconDirective, OrgIconDirective,
@@ -151,12 +150,41 @@ export class VaultListItemsContainerComponent implements OnInit, AfterViewInit {
@Output() @Output()
onRefresh = new EventEmitter<void>(); 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. * Option to show the autofill button for each item.
*/ */
@Input({ transform: booleanAttribute }) @Input({ transform: booleanAttribute })
showAutofillButton: boolean; 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. * 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. * Launches the login cipher in a new browser tab.
*/ */

View File

@@ -22,6 +22,11 @@
</bit-no-items> </bit-no-items>
</div> </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 --> <!-- Show search & filters outside of the scroll area of the page -->
<ng-container <ng-container
slot="above-scroll-area" 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 { VaultPopupListFiltersService } from "../../services/vault-popup-list-filters.service";
import { VaultUiOnboardingService } from "../../services/vault-ui-onboarding.service"; import { VaultUiOnboardingService } from "../../services/vault-ui-onboarding.service";
import { BlockedInjectionBanner } from "./blocked-injection-banner/blocked-injection-banner.component";
import { import {
NewItemDropdownV2Component, NewItemDropdownV2Component,
NewItemInitialValues, NewItemInitialValues,
@@ -40,6 +41,7 @@ enum VaultState {
templateUrl: "vault-v2.component.html", templateUrl: "vault-v2.component.html",
standalone: true, standalone: true,
imports: [ imports: [
BlockedInjectionBanner,
PopupPageComponent, PopupPageComponent,
PopupHeaderComponent, PopupHeaderComponent,
PopOutComponent, PopOutComponent,

View File

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

View File

@@ -15,6 +15,7 @@ import {
} from "rxjs"; } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; 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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
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";
@@ -67,6 +68,72 @@ export class VaultPopupAutofillService {
shareReplay({ refCount: false, bufferSize: 1 }), 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. * 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. * 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( constructor(
private autofillService: AutofillService, private autofillService: AutofillService,
private domainSettingsService: DomainSettingsService,
private i18nService: I18nService, private i18nService: I18nService,
private toastService: ToastService, private toastService: ToastService,
private platformUtilService: PlatformUtilsService, private platformUtilService: PlatformUtilsService,