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:
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user