From d587be1831601ec6482a00ad6647579ab90a31ad Mon Sep 17 00:00:00 2001
From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com>
Date: Wed, 25 Sep 2024 04:07:01 -0700
Subject: [PATCH 1/9] [PM-12403] - Implement Remove Send policy on Add/edit
screen (#11178)
* disable edit send if policy requires
* remove unused var
* don't display free bitwarden families button
* Revert "don't display free bitwarden families button"
This reverts commit 832564d705e082efcd6c7938cc7c1d9c6b23def4.
* use config instead of policy service
* Revert "don't display free bitwarden families button"
This reverts commit 832564d705e082efcd6c7938cc7c1d9c6b23def4.
* remove unnecessary code
* Use short when transforming deletionDate instead of fixed format
---------
Co-authored-by: Daniel James Smith
---
.../options/send-options.component.ts | 4 +
.../base-send-details.component.ts | 107 ------------------
.../send-details/send-details.component.ts | 105 ++++++++++++++---
.../send-file-details.component.ts | 4 +
.../send-text-details.component.ts | 4 +
5 files changed, 104 insertions(+), 120 deletions(-)
delete mode 100644 libs/tools/send/send-ui/src/send-form/components/send-details/base-send-details.component.ts
diff --git a/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.ts b/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.ts
index 89ab9d19ba..a73a3a6ad8 100644
--- a/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.ts
+++ b/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.ts
@@ -97,6 +97,7 @@ export class SendOptionsComponent implements OnInit {
});
});
}
+
ngOnInit() {
if (this.sendFormContainer.originalSendView) {
this.sendOptionsForm.patchValue({
@@ -107,5 +108,8 @@ export class SendOptionsComponent implements OnInit {
notes: this.sendFormContainer.originalSendView.notes,
});
}
+ if (!this.config.areSendsAllowed) {
+ this.sendOptionsForm.disable();
+ }
}
}
diff --git a/libs/tools/send/send-ui/src/send-form/components/send-details/base-send-details.component.ts b/libs/tools/send/send-ui/src/send-form/components/send-details/base-send-details.component.ts
deleted file mode 100644
index b5cf8ee0c7..0000000000
--- a/libs/tools/send/send-ui/src/send-form/components/send-details/base-send-details.component.ts
+++ /dev/null
@@ -1,107 +0,0 @@
-import { DatePipe } from "@angular/common";
-import { Component, Input, OnInit } from "@angular/core";
-import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
-import { FormBuilder, FormControl, Validators } from "@angular/forms";
-
-import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
-import { SendView } from "@bitwarden/common/tools/send/models/view/send.view";
-
-import { SendFormConfig } from "../../abstractions/send-form-config.service";
-import { SendFormContainer } from "../../send-form-container";
-
-// Value = hours
-export enum DatePreset {
- OneHour = 1,
- OneDay = 24,
- TwoDays = 48,
- ThreeDays = 72,
- SevenDays = 168,
- FourteenDays = 336,
- ThirtyDays = 720,
-}
-
-export interface DatePresetSelectOption {
- name: string;
- value: DatePreset | string;
-}
-
-@Component({
- selector: "base-send-details-behavior",
- template: "",
-})
-export class BaseSendDetailsComponent implements OnInit {
- @Input() config: SendFormConfig;
- @Input() originalSendView?: SendView;
-
- customDeletionDateOption: DatePresetSelectOption | null = null;
- datePresetOptions: DatePresetSelectOption[] = [];
-
- sendDetailsForm = this.formBuilder.group({
- name: new FormControl("", Validators.required),
- selectedDeletionDatePreset: new FormControl(DatePreset.SevenDays || "", Validators.required),
- });
-
- constructor(
- protected sendFormContainer: SendFormContainer,
- protected formBuilder: FormBuilder,
- protected i18nService: I18nService,
- protected datePipe: DatePipe,
- ) {
- this.sendDetailsForm.valueChanges.pipe(takeUntilDestroyed()).subscribe((value) => {
- this.sendFormContainer.patchSend((send) => {
- return Object.assign(send, {
- name: value.name,
- deletionDate: new Date(this.formattedDeletionDate),
- expirationDate: new Date(this.formattedDeletionDate),
- } as SendView);
- });
- });
-
- this.sendFormContainer.registerChildForm("sendDetailsForm", this.sendDetailsForm);
- }
-
- async ngOnInit() {
- this.setupDeletionDatePresets();
-
- if (this.originalSendView) {
- this.sendDetailsForm.patchValue({
- name: this.originalSendView.name,
- selectedDeletionDatePreset: this.originalSendView.deletionDate.toString(),
- });
-
- if (this.originalSendView.deletionDate) {
- this.customDeletionDateOption = {
- name: this.datePipe.transform(this.originalSendView.deletionDate, "MM/dd/yyyy, hh:mm a"),
- value: this.originalSendView.deletionDate.toString(),
- };
- this.datePresetOptions.unshift(this.customDeletionDateOption);
- }
- }
- }
-
- setupDeletionDatePresets() {
- const defaultSelections: DatePresetSelectOption[] = [
- { name: this.i18nService.t("oneHour"), value: DatePreset.OneHour },
- { name: this.i18nService.t("oneDay"), value: DatePreset.OneDay },
- { name: this.i18nService.t("days", "2"), value: DatePreset.TwoDays },
- { name: this.i18nService.t("days", "3"), value: DatePreset.ThreeDays },
- { name: this.i18nService.t("days", "7"), value: DatePreset.SevenDays },
- { name: this.i18nService.t("days", "14"), value: DatePreset.FourteenDays },
- { name: this.i18nService.t("days", "30"), value: DatePreset.ThirtyDays },
- ];
-
- this.datePresetOptions = defaultSelections;
- }
-
- get formattedDeletionDate(): string {
- const now = new Date();
- const selectedValue = this.sendDetailsForm.controls.selectedDeletionDatePreset.value;
-
- if (typeof selectedValue === "string") {
- return selectedValue;
- }
-
- const milliseconds = now.setTime(now.getTime() + (selectedValue as number) * 60 * 60 * 1000);
- return new Date(milliseconds).toString();
- }
-}
diff --git a/libs/tools/send/send-ui/src/send-form/components/send-details/send-details.component.ts b/libs/tools/send/send-ui/src/send-form/components/send-details/send-details.component.ts
index 0b287205be..68a5e40a57 100644
--- a/libs/tools/send/send-ui/src/send-form/components/send-details/send-details.component.ts
+++ b/libs/tools/send/send-ui/src/send-form/components/send-details/send-details.component.ts
@@ -1,12 +1,14 @@
import { CommonModule, DatePipe } from "@angular/common";
-import { Component, OnInit } from "@angular/core";
-import { FormBuilder, ReactiveFormsModule } from "@angular/forms";
+import { Component, OnInit, Input } from "@angular/core";
+import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
+import { FormBuilder, FormControl, ReactiveFormsModule, Validators } from "@angular/forms";
import { firstValueFrom } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { SendType } from "@bitwarden/common/tools/send/enums/send-type";
+import { SendView } from "@bitwarden/common/tools/send/models/view/send.view";
import {
SectionComponent,
SectionHeaderComponent,
@@ -18,13 +20,29 @@ import {
SelectModule,
} from "@bitwarden/components";
+import { SendFormConfig } from "../../abstractions/send-form-config.service";
import { SendFormContainer } from "../../send-form-container";
import { SendOptionsComponent } from "../options/send-options.component";
-import { BaseSendDetailsComponent } from "./base-send-details.component";
import { SendFileDetailsComponent } from "./send-file-details.component";
import { SendTextDetailsComponent } from "./send-text-details.component";
+// Value = hours
+export enum DatePreset {
+ OneHour = 1,
+ OneDay = 24,
+ TwoDays = 48,
+ ThreeDays = 72,
+ SevenDays = 168,
+ FourteenDays = 336,
+ ThirtyDays = 720,
+}
+
+export interface DatePresetSelectOption {
+ name: string;
+ value: DatePreset | string;
+}
+
@Component({
selector: "tools-send-details",
templateUrl: "./send-details.component.html",
@@ -46,10 +64,20 @@ import { SendTextDetailsComponent } from "./send-text-details.component";
SelectModule,
],
})
-export class SendDetailsComponent extends BaseSendDetailsComponent implements OnInit {
+export class SendDetailsComponent implements OnInit {
+ @Input() config: SendFormConfig;
+ @Input() originalSendView?: SendView;
+
FileSendType = SendType.File;
TextSendType = SendType.Text;
sendLink: string | null = null;
+ customDeletionDateOption: DatePresetSelectOption | null = null;
+ datePresetOptions: DatePresetSelectOption[] = [];
+
+ sendDetailsForm = this.formBuilder.group({
+ name: new FormControl("", Validators.required),
+ selectedDeletionDatePreset: new FormControl(DatePreset.SevenDays || "", Validators.required),
+ });
constructor(
protected sendFormContainer: SendFormContainer,
@@ -58,18 +86,69 @@ export class SendDetailsComponent extends BaseSendDetailsComponent implements On
protected datePipe: DatePipe,
protected environmentService: EnvironmentService,
) {
- super(sendFormContainer, formBuilder, i18nService, datePipe);
- }
+ this.sendDetailsForm.valueChanges.pipe(takeUntilDestroyed()).subscribe((value) => {
+ this.sendFormContainer.patchSend((send) => {
+ return Object.assign(send, {
+ name: value.name,
+ deletionDate: new Date(this.formattedDeletionDate),
+ expirationDate: new Date(this.formattedDeletionDate),
+ } as SendView);
+ });
+ });
- async getSendLink() {}
+ this.sendFormContainer.registerChildForm("sendDetailsForm", this.sendDetailsForm);
+ }
async ngOnInit() {
- await super.ngOnInit();
- if (!this.originalSendView) {
- return;
+ this.setupDeletionDatePresets();
+
+ if (this.originalSendView) {
+ this.sendDetailsForm.patchValue({
+ name: this.originalSendView.name,
+ selectedDeletionDatePreset: this.originalSendView.deletionDate.toString(),
+ });
+
+ if (this.originalSendView.deletionDate) {
+ this.customDeletionDateOption = {
+ name: this.datePipe.transform(this.originalSendView.deletionDate, "short"),
+ value: this.originalSendView.deletionDate.toString(),
+ };
+ this.datePresetOptions.unshift(this.customDeletionDateOption);
+ }
+
+ const env = await firstValueFrom(this.environmentService.environment$);
+ this.sendLink =
+ env.getSendUrl() + this.originalSendView.accessId + "/" + this.originalSendView.urlB64Key;
}
- const env = await firstValueFrom(this.environmentService.environment$);
- this.sendLink =
- env.getSendUrl() + this.originalSendView.accessId + "/" + this.originalSendView.urlB64Key;
+
+ if (!this.config.areSendsAllowed) {
+ this.sendDetailsForm.disable();
+ }
+ }
+
+ setupDeletionDatePresets() {
+ const defaultSelections: DatePresetSelectOption[] = [
+ { name: this.i18nService.t("oneHour"), value: DatePreset.OneHour },
+ { name: this.i18nService.t("oneDay"), value: DatePreset.OneDay },
+ { name: this.i18nService.t("days", "2"), value: DatePreset.TwoDays },
+ { name: this.i18nService.t("days", "3"), value: DatePreset.ThreeDays },
+ { name: this.i18nService.t("days", "7"), value: DatePreset.SevenDays },
+ { name: this.i18nService.t("days", "14"), value: DatePreset.FourteenDays },
+ { name: this.i18nService.t("days", "30"), value: DatePreset.ThirtyDays },
+ ];
+
+ this.datePresetOptions = defaultSelections;
+ }
+
+ get formattedDeletionDate(): string {
+ const now = new Date();
+ const selectedValue = this.sendDetailsForm.controls.selectedDeletionDatePreset.value;
+
+ if (typeof selectedValue === "string") {
+ return selectedValue;
+ }
+
+ const milliseconds = now.setTime(now.getTime() + (selectedValue as number) * 60 * 60 * 1000);
+ return new Date(milliseconds).toString();
}
}
diff --git a/libs/tools/send/send-ui/src/send-form/components/send-details/send-file-details.component.ts b/libs/tools/send/send-ui/src/send-form/components/send-details/send-file-details.component.ts
index 7739ca2652..447256cacd 100644
--- a/libs/tools/send/send-ui/src/send-form/components/send-details/send-file-details.component.ts
+++ b/libs/tools/send/send-ui/src/send-form/components/send-details/send-file-details.component.ts
@@ -73,5 +73,9 @@ export class SendFileDetailsComponent implements OnInit {
file: this.originalSendView.file,
});
}
+
+ if (!this.config.areSendsAllowed) {
+ this.sendFileDetailsForm.disable();
+ }
}
}
diff --git a/libs/tools/send/send-ui/src/send-form/components/send-details/send-text-details.component.ts b/libs/tools/send/send-ui/src/send-form/components/send-details/send-text-details.component.ts
index 85fc324f2f..873f85c9e3 100644
--- a/libs/tools/send/send-ui/src/send-form/components/send-details/send-text-details.component.ts
+++ b/libs/tools/send/send-ui/src/send-form/components/send-details/send-text-details.component.ts
@@ -57,5 +57,9 @@ export class SendTextDetailsComponent implements OnInit {
hidden: this.originalSendView.text?.hidden || false,
});
}
+
+ if (!this.config.areSendsAllowed) {
+ this.sendTextDetailsForm.disable();
+ }
}
}
From cd9045483bc31768698b197d9452b5b01da68c99 Mon Sep 17 00:00:00 2001
From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com>
Date: Wed, 25 Sep 2024 05:03:42 -0700
Subject: [PATCH 2/9] [PM-12561] - add data attrs for send form (#11209)
* add data attrs for send form
* Add data-testid for toggle view password
* Revert "Add data-testid for toggle view password"
This reverts commit bd6fcc8c1bf6d4d61888ace8480d34b3098d0770.
* move dataid to component
---------
Co-authored-by: Daniel James Smith
---
.../components/options/send-options.component.html | 9 ++++++++-
.../components/send-details/send-details.component.html | 2 +-
.../send-details/send-file-details.component.html | 4 ++--
3 files changed, 11 insertions(+), 4 deletions(-)
diff --git a/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.html b/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.html
index 53483065b7..01b96e3bc5 100644
--- a/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.html
+++ b/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.html
@@ -15,12 +15,19 @@
{{ "password" | i18n }}
{{ "newPassword" | i18n }}
-
+
{{ "sendPasswordDescV2" | i18n }}
diff --git a/libs/tools/send/send-ui/src/send-form/components/send-details/send-details.component.html b/libs/tools/send/send-ui/src/send-form/components/send-details/send-details.component.html
index 98f399760b..d4c253303b 100644
--- a/libs/tools/send/send-ui/src/send-form/components/send-details/send-details.component.html
+++ b/libs/tools/send/send-ui/src/send-form/components/send-details/send-details.component.html
@@ -23,7 +23,7 @@
{{ "sendLink" | i18n }}
-
+
{{ "passwordHistory" | i18n }}
diff --git a/libs/vault/src/cipher-view/item-history/item-history-v2.component.ts b/libs/vault/src/cipher-view/item-history/item-history-v2.component.ts
index 55c8b90da1..4a37c9a491 100644
--- a/libs/vault/src/cipher-view/item-history/item-history-v2.component.ts
+++ b/libs/vault/src/cipher-view/item-history/item-history-v2.component.ts
@@ -3,6 +3,8 @@ import { Component, Input } from "@angular/core";
import { RouterModule } from "@angular/router";
import { JslibModule } from "@bitwarden/angular/jslib.module";
+import { CipherId } from "@bitwarden/common/types/guid";
+import { ViewPasswordHistoryService } from "@bitwarden/common/vault/abstractions/view-password-history.service";
import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import {
@@ -31,7 +33,16 @@ import {
export class ItemHistoryV2Component {
@Input() cipher: CipherView;
+ constructor(private viewPasswordHistoryService: ViewPasswordHistoryService) {}
+
get isLogin() {
return this.cipher.type === CipherType.Login;
}
+
+ /**
+ * View the password history for the cipher.
+ */
+ async viewPasswordHistory() {
+ await this.viewPasswordHistoryService.viewPasswordHistory(this.cipher?.id as CipherId);
+ }
}
From d0b09202c6684fd3b5482baf3abfa49a788908ce Mon Sep 17 00:00:00 2001
From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com>
Date: Wed, 25 Sep 2024 10:40:23 -0700
Subject: [PATCH 5/9] [PM-12504] - hide create send button and send tab when
sends are disabled (#11186)
* hide create send button and send tab when sends are disabled
* reverse logic
* tidy up filter.
* fix popup tab navigation filter
* fix popup tab nav state
* fix popup-layout stories
---
.../popup/layout/popup-layout.stories.ts | 26 +++++++
.../layout/popup-tab-navigation.component.ts | 77 ++++++++++++-------
.../popup/send-v2/send-v2.component.html | 19 +++--
.../popup/send-v2/send-v2.component.spec.ts | 6 ++
.../tools/popup/send-v2/send-v2.component.ts | 15 +++-
5 files changed, 108 insertions(+), 35 deletions(-)
diff --git a/apps/browser/src/platform/popup/layout/popup-layout.stories.ts b/apps/browser/src/platform/popup/layout/popup-layout.stories.ts
index affa804cc7..4851541576 100644
--- a/apps/browser/src/platform/popup/layout/popup-layout.stories.ts
+++ b/apps/browser/src/platform/popup/layout/popup-layout.stories.ts
@@ -3,7 +3,9 @@ import { Component, importProvidersFrom } from "@angular/core";
import { RouterModule } from "@angular/router";
import { Meta, StoryObj, applicationConfig, moduleMetadata } from "@storybook/angular";
+import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
+import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
import {
AvatarModule,
BadgeModule,
@@ -318,6 +320,30 @@ export default {
});
},
},
+ {
+ provide: PolicyService,
+ useFactory: () => {
+ return {
+ policyAppliesToActiveUser$: () => {
+ return {
+ pipe: () => ({
+ subscribe: () => ({}),
+ }),
+ };
+ },
+ };
+ },
+ },
+ {
+ provide: SendService,
+ useFactory: () => {
+ return {
+ sends$: () => {
+ return { pipe: () => ({}) };
+ },
+ };
+ },
+ },
],
}),
applicationConfig({
diff --git a/apps/browser/src/platform/popup/layout/popup-tab-navigation.component.ts b/apps/browser/src/platform/popup/layout/popup-tab-navigation.component.ts
index ced3f6462e..8463bbe6e9 100644
--- a/apps/browser/src/platform/popup/layout/popup-tab-navigation.component.ts
+++ b/apps/browser/src/platform/popup/layout/popup-tab-navigation.component.ts
@@ -1,9 +1,41 @@
import { CommonModule } from "@angular/common";
import { Component } from "@angular/core";
+import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { RouterModule } from "@angular/router";
+import { filter, map, switchMap } from "rxjs";
+import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
+import { PolicyType } from "@bitwarden/common/admin-console/enums";
+import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
import { LinkModule } from "@bitwarden/components";
+const allNavButtons = [
+ {
+ label: "Vault",
+ page: "/tabs/vault",
+ iconKey: "lock",
+ iconKeyActive: "lock-f",
+ },
+ {
+ label: "Generator",
+ page: "/tabs/generator",
+ iconKey: "generate",
+ iconKeyActive: "generate-f",
+ },
+ {
+ label: "Send",
+ page: "/tabs/send",
+ iconKey: "send",
+ iconKeyActive: "send-f",
+ },
+ {
+ label: "Settings",
+ page: "/tabs/settings",
+ iconKey: "cog",
+ iconKeyActive: "cog-f",
+ },
+];
+
@Component({
selector: "popup-tab-navigation",
templateUrl: "popup-tab-navigation.component.html",
@@ -14,30 +46,23 @@ import { LinkModule } from "@bitwarden/components";
},
})
export class PopupTabNavigationComponent {
- navButtons = [
- {
- label: "Vault",
- page: "/tabs/vault",
- iconKey: "lock",
- iconKeyActive: "lock-f",
- },
- {
- label: "Generator",
- page: "/tabs/generator",
- iconKey: "generate",
- iconKeyActive: "generate-f",
- },
- {
- label: "Send",
- page: "/tabs/send",
- iconKey: "send",
- iconKeyActive: "send-f",
- },
- {
- label: "Settings",
- page: "/tabs/settings",
- iconKey: "cog",
- iconKeyActive: "cog-f",
- },
- ];
+ navButtons = allNavButtons;
+ constructor(
+ private policyService: PolicyService,
+ private sendService: SendService,
+ ) {
+ this.policyService
+ .policyAppliesToActiveUser$(PolicyType.DisableSend)
+ .pipe(
+ filter((policyAppliesToActiveUser) => policyAppliesToActiveUser),
+ switchMap(() => this.sendService.sends$),
+ map((sends) => sends.length > 1),
+ takeUntilDestroyed(),
+ )
+ .subscribe((hasSends) => {
+ this.navButtons = hasSends
+ ? allNavButtons
+ : allNavButtons.filter((b) => b.page !== "/tabs/send");
+ });
+ }
}
diff --git a/apps/browser/src/tools/popup/send-v2/send-v2.component.html b/apps/browser/src/tools/popup/send-v2/send-v2.component.html
index a8dd3e24f2..698901d846 100644
--- a/apps/browser/src/tools/popup/send-v2/send-v2.component.html
+++ b/apps/browser/src/tools/popup/send-v2/send-v2.component.html
@@ -1,12 +1,20 @@
-
-
+
+
+
+ {{ "sendDisabledWarning" | i18n }}
+
+
+
+
+
+
{{ "sendsNoItemsTitle" | i18n }}
{{ "sendsNoItemsMessage" | i18n }}
-
+
@@ -31,9 +39,4 @@
-
-
diff --git a/apps/browser/src/tools/popup/send-v2/send-v2.component.spec.ts b/apps/browser/src/tools/popup/send-v2/send-v2.component.spec.ts
index 50e5531743..63e3c2d2fc 100644
--- a/apps/browser/src/tools/popup/send-v2/send-v2.component.spec.ts
+++ b/apps/browser/src/tools/popup/send-v2/send-v2.component.spec.ts
@@ -7,6 +7,7 @@ import { of, BehaviorSubject } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { SearchService } from "@bitwarden/common/abstractions/search.service";
+import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { AvatarService } from "@bitwarden/common/auth/abstractions/avatar.service";
@@ -46,6 +47,7 @@ describe("SendV2Component", () => {
let sendListFiltersServiceFilters$: BehaviorSubject<{ sendType: SendType | null }>;
let sendItemsServiceEmptyList$: BehaviorSubject;
let sendItemsServiceNoFilteredResults$: BehaviorSubject;
+ let policyService: MockProxy;
beforeEach(async () => {
sendListFiltersServiceFilters$ = new BehaviorSubject({ sendType: null });
@@ -60,6 +62,9 @@ describe("SendV2Component", () => {
latestSearchText$: of(""),
});
+ policyService = mock();
+ policyService.policyAppliesToActiveUser$.mockReturnValue(of(true)); // Return `true` by default
+
sendListFiltersService = new SendListFiltersService(mock(), new FormBuilder());
sendListFiltersService.filters$ = sendListFiltersServiceFilters$;
@@ -104,6 +109,7 @@ describe("SendV2Component", () => {
{ provide: I18nService, useValue: { t: (key: string) => key } },
{ provide: SendListFiltersService, useValue: sendListFiltersService },
{ provide: PopupRouterCacheService, useValue: mock() },
+ { provide: PolicyService, useValue: policyService },
],
}).compileComponents();
diff --git a/apps/browser/src/tools/popup/send-v2/send-v2.component.ts b/apps/browser/src/tools/popup/send-v2/send-v2.component.ts
index 5c1ec89fde..19fff402e2 100644
--- a/apps/browser/src/tools/popup/send-v2/send-v2.component.ts
+++ b/apps/browser/src/tools/popup/send-v2/send-v2.component.ts
@@ -5,8 +5,10 @@ import { RouterLink } from "@angular/router";
import { combineLatest } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
+import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
+import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { SendType } from "@bitwarden/common/tools/send/enums/send-type";
-import { ButtonModule, Icons, NoItemsModule } from "@bitwarden/components";
+import { ButtonModule, CalloutModule, Icons, NoItemsModule } from "@bitwarden/components";
import {
NoSendsIcon,
NewSendDropdownComponent,
@@ -31,6 +33,7 @@ export enum SendState {
templateUrl: "send-v2.component.html",
standalone: true,
imports: [
+ CalloutModule,
PopupPageComponent,
PopupHeaderComponent,
PopOutComponent,
@@ -61,9 +64,12 @@ export class SendV2Component implements OnInit, OnDestroy {
protected noResultsIcon = Icons.NoResults;
+ protected sendsDisabled = false;
+
constructor(
protected sendItemsService: SendItemsService,
protected sendListFiltersService: SendListFiltersService,
+ private policyService: PolicyService,
) {
combineLatest([
this.sendItemsService.emptyList$,
@@ -90,6 +96,13 @@ export class SendV2Component implements OnInit, OnDestroy {
this.listState = null;
});
+
+ this.policyService
+ .policyAppliesToActiveUser$(PolicyType.DisableSend)
+ .pipe(takeUntilDestroyed())
+ .subscribe((sendsDisabled) => {
+ this.sendsDisabled = sendsDisabled;
+ });
}
ngOnInit(): void {}
From 7f339543165b351d2c5e4de8857a9d3a39131309 Mon Sep 17 00:00:00 2001
From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com>
Date: Wed, 25 Sep 2024 10:56:04 -0700
Subject: [PATCH 6/9] don't display free bitwarden families button (#11184)
---
.../about-page/more-from-bitwarden-page-v2.component.ts | 2 +-
.../organization/organization.service.abstraction.ts | 4 ++++
.../services/organization/organization.service.ts | 4 ++++
3 files changed, 9 insertions(+), 1 deletion(-)
diff --git a/apps/browser/src/tools/popup/settings/about-page/more-from-bitwarden-page-v2.component.ts b/apps/browser/src/tools/popup/settings/about-page/more-from-bitwarden-page-v2.component.ts
index 0f05480ea1..7cdb691d56 100644
--- a/apps/browser/src/tools/popup/settings/about-page/more-from-bitwarden-page-v2.component.ts
+++ b/apps/browser/src/tools/popup/settings/about-page/more-from-bitwarden-page-v2.component.ts
@@ -38,7 +38,7 @@ export class MoreFromBitwardenPageV2Component {
private organizationService: OrganizationService,
) {
this.canAccessPremium$ = billingAccountProfileStateService.hasPremiumFromAnySource$;
- this.familySponsorshipAvailable$ = this.organizationService.canManageSponsorships$;
+ this.familySponsorshipAvailable$ = this.organizationService.familySponsorshipAvailable$;
}
async openFreeBitwardenFamiliesPage() {
diff --git a/libs/common/src/admin-console/abstractions/organization/organization.service.abstraction.ts b/libs/common/src/admin-console/abstractions/organization/organization.service.abstraction.ts
index 0cea2aee53..a2ea6aa886 100644
--- a/libs/common/src/admin-console/abstractions/organization/organization.service.abstraction.ts
+++ b/libs/common/src/admin-console/abstractions/organization/organization.service.abstraction.ts
@@ -117,6 +117,10 @@ export abstract class OrganizationService {
* Emits true if the user can create or manage a Free Bitwarden Families sponsorship.
*/
canManageSponsorships$: Observable;
+ /**
+ * Emits true if any of the user's organizations have a Free Bitwarden Families sponsorship available.
+ */
+ familySponsorshipAvailable$: Observable;
hasOrganizations: () => Promise;
get$: (id: string) => Observable;
get: (id: string) => Promise;
diff --git a/libs/common/src/admin-console/services/organization/organization.service.ts b/libs/common/src/admin-console/services/organization/organization.service.ts
index d8fe18dc5c..91bfcbd0d5 100644
--- a/libs/common/src/admin-console/services/organization/organization.service.ts
+++ b/libs/common/src/admin-console/services/organization/organization.service.ts
@@ -88,6 +88,10 @@ export class OrganizationService implements InternalOrganizationServiceAbstracti
mapToBooleanHasAnyOrganizations(),
);
+ familySponsorshipAvailable$ = this.organizations$.pipe(
+ map((orgs) => orgs.some((o) => o.familySponsorshipAvailable)),
+ );
+
async hasOrganizations(): Promise {
return await firstValueFrom(this.organizations$.pipe(mapToBooleanHasAnyOrganizations()));
}
From caece397c68055666b5323aaf02dd7d35a1260b0 Mon Sep 17 00:00:00 2001
From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com>
Date: Wed, 25 Sep 2024 12:35:12 -0700
Subject: [PATCH 7/9] [PM-11927] - File Send popout dialog (#11138)
* file popout component
* finish file popout dialog
* finalize send popout dialog component
* fix tests
* conditionally provide file popout dialog
* simplify send file popout dialog
* add file popout dialog container
* remove unnecessary modules
---
apps/browser/src/_locales/en/messages.json | 11 +++++++
.../add-edit/send-add-edit.component.html | 2 ++
.../add-edit/send-add-edit.component.ts | 2 ++
...ile-popout-dialog-container.component.html | 0
...-file-popout-dialog-container.component.ts | 31 +++++++++++++++++++
.../send-file-popout-dialog.component.html | 20 ++++++++++++
.../send-file-popout-dialog.component.ts | 25 +++++++++++++++
.../tools/popup/send-v2/send-v2.component.ts | 5 ---
8 files changed, 91 insertions(+), 5 deletions(-)
create mode 100644 apps/browser/src/tools/popup/send-v2/send-file-popout-dialog/send-file-popout-dialog-container.component.html
create mode 100644 apps/browser/src/tools/popup/send-v2/send-file-popout-dialog/send-file-popout-dialog-container.component.ts
create mode 100644 apps/browser/src/tools/popup/send-v2/send-file-popout-dialog/send-file-popout-dialog.component.html
create mode 100644 apps/browser/src/tools/popup/send-v2/send-file-popout-dialog/send-file-popout-dialog.component.ts
diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json
index 8c08caec35..49b5eb82bb 100644
--- a/apps/browser/src/_locales/en/messages.json
+++ b/apps/browser/src/_locales/en/messages.json
@@ -2504,6 +2504,14 @@
"message": "Send saved",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
+ "sendFilePopoutDialogText": {
+ "message": "Pop out extension?",
+ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
+ },
+ "sendFilePopoutDialogDesc": {
+ "message": "To create a file Send, you need to pop out te extension to a new window.",
+ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
+ },
"sendLinuxChromiumFileWarning": {
"message": "In order to choose a file, open the extension in the sidebar (if possible) or pop out to a new window by clicking this banner."
},
@@ -2513,6 +2521,9 @@
"sendSafariFileWarning": {
"message": "In order to choose a file using Safari, pop out to a new window by clicking this banner."
},
+ "popOut": {
+ "message": "Pop out"
+ },
"sendFileCalloutHeader": {
"message": "Before you start"
},
diff --git a/apps/browser/src/tools/popup/send-v2/add-edit/send-add-edit.component.html b/apps/browser/src/tools/popup/send-v2/add-edit/send-add-edit.component.html
index 7f723cc736..e96a0742a0 100644
--- a/apps/browser/src/tools/popup/send-v2/add-edit/send-add-edit.component.html
+++ b/apps/browser/src/tools/popup/send-v2/add-edit/send-add-edit.component.html
@@ -9,6 +9,8 @@
>
+
+
{{ "save" | i18n }}
diff --git a/apps/browser/src/tools/popup/send-v2/add-edit/send-add-edit.component.ts b/apps/browser/src/tools/popup/send-v2/add-edit/send-add-edit.component.ts
index 91311ab7e7..c84b9717df 100644
--- a/apps/browser/src/tools/popup/send-v2/add-edit/send-add-edit.component.ts
+++ b/apps/browser/src/tools/popup/send-v2/add-edit/send-add-edit.component.ts
@@ -29,6 +29,7 @@ import { SendFormModule } from "../../../../../../../libs/tools/send/send-ui/src
import { PopupFooterComponent } from "../../../../platform/popup/layout/popup-footer.component";
import { PopupHeaderComponent } from "../../../../platform/popup/layout/popup-header.component";
import { PopupPageComponent } from "../../../../platform/popup/layout/popup-page.component";
+import { SendFilePopoutDialogContainerComponent } from "../send-file-popout-dialog/send-file-popout-dialog-container.component";
/**
* Helper class to parse query parameters for the AddEdit route.
@@ -70,6 +71,7 @@ export type AddEditQueryParams = Partial>;
PopupPageComponent,
PopupHeaderComponent,
PopupFooterComponent,
+ SendFilePopoutDialogContainerComponent,
SendFormModule,
AsyncActionsModule,
],
diff --git a/apps/browser/src/tools/popup/send-v2/send-file-popout-dialog/send-file-popout-dialog-container.component.html b/apps/browser/src/tools/popup/send-v2/send-file-popout-dialog/send-file-popout-dialog-container.component.html
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/apps/browser/src/tools/popup/send-v2/send-file-popout-dialog/send-file-popout-dialog-container.component.ts b/apps/browser/src/tools/popup/send-v2/send-file-popout-dialog/send-file-popout-dialog-container.component.ts
new file mode 100644
index 0000000000..33f121431f
--- /dev/null
+++ b/apps/browser/src/tools/popup/send-v2/send-file-popout-dialog/send-file-popout-dialog-container.component.ts
@@ -0,0 +1,31 @@
+import { CommonModule } from "@angular/common";
+import { Component, Input, OnInit } from "@angular/core";
+
+import { JslibModule } from "@bitwarden/angular/jslib.module";
+import { DialogService } from "@bitwarden/components";
+import { SendFormConfig } from "@bitwarden/send-ui";
+
+import { FilePopoutUtilsService } from "../../services/file-popout-utils.service";
+
+import { SendFilePopoutDialogComponent } from "./send-file-popout-dialog.component";
+
+@Component({
+ selector: "send-file-popout-dialog-container",
+ templateUrl: "./send-file-popout-dialog-container.component.html",
+ standalone: true,
+ imports: [JslibModule, CommonModule],
+})
+export class SendFilePopoutDialogContainerComponent implements OnInit {
+ @Input() config: SendFormConfig;
+
+ constructor(
+ private dialogService: DialogService,
+ private filePopoutUtilsService: FilePopoutUtilsService,
+ ) {}
+
+ ngOnInit() {
+ if (this.config.mode === "add" && this.filePopoutUtilsService.showFilePopoutMessage(window)) {
+ this.dialogService.open(SendFilePopoutDialogComponent);
+ }
+ }
+}
diff --git a/apps/browser/src/tools/popup/send-v2/send-file-popout-dialog/send-file-popout-dialog.component.html b/apps/browser/src/tools/popup/send-v2/send-file-popout-dialog/send-file-popout-dialog.component.html
new file mode 100644
index 0000000000..aaede0a821
--- /dev/null
+++ b/apps/browser/src/tools/popup/send-v2/send-file-popout-dialog/send-file-popout-dialog.component.html
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+ {{ "sendFilePopoutDialogText" | i18n }}
+
+ {{ "sendFilePopoutDialogDesc" | i18n }}
+
+
+
+ {{ "popOut" | i18n }}
+
+
+
+ {{ "cancel" | i18n }}
+
+
+
diff --git a/apps/browser/src/tools/popup/send-v2/send-file-popout-dialog/send-file-popout-dialog.component.ts b/apps/browser/src/tools/popup/send-v2/send-file-popout-dialog/send-file-popout-dialog.component.ts
new file mode 100644
index 0000000000..fb21b5bb02
--- /dev/null
+++ b/apps/browser/src/tools/popup/send-v2/send-file-popout-dialog/send-file-popout-dialog.component.ts
@@ -0,0 +1,25 @@
+import { CommonModule } from "@angular/common";
+import { Component } from "@angular/core";
+
+import { JslibModule } from "@bitwarden/angular/jslib.module";
+import { ButtonModule, DialogModule, DialogService, TypographyModule } from "@bitwarden/components";
+
+import BrowserPopupUtils from "../../../../platform/popup/browser-popup-utils";
+
+@Component({
+ selector: "send-file-popout-dialog",
+ templateUrl: "./send-file-popout-dialog.component.html",
+ standalone: true,
+ imports: [JslibModule, CommonModule, DialogModule, ButtonModule, TypographyModule],
+})
+export class SendFilePopoutDialogComponent {
+ constructor(private dialogService: DialogService) {}
+
+ async popOutWindow() {
+ await BrowserPopupUtils.openCurrentPagePopout(window);
+ }
+
+ close() {
+ this.dialogService.closeAll();
+ }
+}
diff --git a/apps/browser/src/tools/popup/send-v2/send-v2.component.ts b/apps/browser/src/tools/popup/send-v2/send-v2.component.ts
index 19fff402e2..26a995e8c4 100644
--- a/apps/browser/src/tools/popup/send-v2/send-v2.component.ts
+++ b/apps/browser/src/tools/popup/send-v2/send-v2.component.ts
@@ -51,17 +51,12 @@ export enum SendState {
})
export class SendV2Component implements OnInit, OnDestroy {
sendType = SendType;
-
sendState = SendState;
protected listState: SendState | null = null;
-
protected sends$ = this.sendItemsService.filteredAndSortedSends$;
-
protected title: string = "allSends";
-
protected noItemIcon = NoSendsIcon;
-
protected noResultsIcon = Icons.NoResults;
protected sendsDisabled = false;
From 077abe0518419fa5c2e55ca543650214ca5994c0 Mon Sep 17 00:00:00 2001
From: Cesar Gonzalez
Date: Wed, 25 Sep 2024 16:56:50 -0500
Subject: [PATCH 8/9] [PM-12316] Implement inline menu passkeys authenticating
state (#11113)
---
apps/browser/src/_locales/en/messages.json | 3 +
.../overlay-notifications.background.ts | 41 +------
.../background/overlay.background.spec.ts | 115 +++++++++++++-----
.../autofill/background/overlay.background.ts | 50 ++++++--
.../autofill-inline-menu-list.spec.ts.snap | 38 ++++++
.../list/autofill-inline-menu-list.spec.ts | 61 ++++++++--
.../pages/list/autofill-inline-menu-list.ts | 89 +++++++++++---
.../overlay/inline-menu/pages/list/list.scss | 39 +++++-
.../src/autofill/services/autofill.service.ts | 6 +-
.../src/autofill/shared/styles/variables.scss | 5 +
apps/browser/src/autofill/utils/index.ts | 47 +++++++
apps/browser/src/autofill/utils/svg-icons.ts | 3 +
12 files changed, 391 insertions(+), 106 deletions(-)
diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json
index 49b5eb82bb..f7db6d78f2 100644
--- a/apps/browser/src/_locales/en/messages.json
+++ b/apps/browser/src/_locales/en/messages.json
@@ -4496,5 +4496,8 @@
},
"noEditPermissions": {
"message": "You don't have permission to edit this item"
+ },
+ "authenticating": {
+ "message": "Authenticating"
}
}
diff --git a/apps/browser/src/autofill/background/overlay-notifications.background.ts b/apps/browser/src/autofill/background/overlay-notifications.background.ts
index e252bdcc4a..ca8f05b77d 100644
--- a/apps/browser/src/autofill/background/overlay-notifications.background.ts
+++ b/apps/browser/src/autofill/background/overlay-notifications.background.ts
@@ -6,6 +6,7 @@ import { ConfigService } from "@bitwarden/common/platform/abstractions/config/co
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { BrowserApi } from "../../platform/browser/browser-api";
+import { generateDomainMatchPatterns, isInvalidResponseStatusCode } from "../utils";
import {
ActiveFormSubmissionRequests,
@@ -109,35 +110,11 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg
*/
private getSenderUrlMatchPatterns(sender: chrome.runtime.MessageSender) {
return new Set([
- ...this.generateMatchPatterns(sender.url),
- ...this.generateMatchPatterns(sender.tab.url),
+ ...generateDomainMatchPatterns(sender.url),
+ ...generateDomainMatchPatterns(sender.tab.url),
]);
}
- /**
- * Generates the origin and subdomain match patterns for the URL.
- *
- * @param url - The URL of the tab
- */
- private generateMatchPatterns(url: string): string[] {
- try {
- if (!url.startsWith("http")) {
- url = `https://${url}`;
- }
-
- const originMatchPattern = `${new URL(url).origin}/*`;
-
- const parsedUrl = new URL(url);
- const splitHost = parsedUrl.hostname.split(".");
- const domain = splitHost.slice(-2).join(".");
- const subDomainMatchPattern = `${parsedUrl.protocol}//*.${domain}/*`;
-
- return [originMatchPattern, subDomainMatchPattern];
- } catch {
- return [];
- }
- }
-
/**
* Stores the login form data that was modified by the user in the content script. This data is
* used to trigger the add login or change password notification when the form is submitted.
@@ -329,7 +306,7 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg
private handleOnCompletedRequestEvent = async (details: chrome.webRequest.WebResponseDetails) => {
if (
this.requestHostIsInvalid(details) ||
- this.isInvalidStatusCode(details.statusCode) ||
+ isInvalidResponseStatusCode(details.statusCode) ||
!this.activeFormSubmissionRequests.has(details.requestId)
) {
return;
@@ -472,16 +449,6 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg
this.setupWebRequestsListeners();
};
- /**
- * Determines if the status code of the web response is invalid. An invalid status code is
- * any status code that is not in the 200-299 range.
- *
- * @param statusCode - The status code of the web response
- */
- private isInvalidStatusCode = (statusCode: number) => {
- return statusCode < 200 || statusCode >= 300;
- };
-
/**
* Determines if the host of the web request is invalid. An invalid host is any host that does not
* start with "http" or a tab id that is less than 0.
diff --git a/apps/browser/src/autofill/background/overlay.background.spec.ts b/apps/browser/src/autofill/background/overlay.background.spec.ts
index 30f19e7260..b6a04f63d5 100644
--- a/apps/browser/src/autofill/background/overlay.background.spec.ts
+++ b/apps/browser/src/autofill/background/overlay.background.spec.ts
@@ -61,6 +61,7 @@ import {
triggerPortOnDisconnectEvent,
triggerPortOnMessageEvent,
triggerWebNavigationOnCommittedEvent,
+ triggerWebRequestOnCompletedEvent,
} from "../spec/testing-utils";
import {
@@ -3003,37 +3004,95 @@ describe("OverlayBackground", () => {
expect(copyToClipboardSpy).toHaveBeenCalledWith("totp-code");
});
- it("triggers passkey authentication through mediated conditional UI", async () => {
- const fido2Credential = mock({ credentialId: "credential-id" });
- const cipher1 = mock({
- id: "inline-menu-cipher-1",
- login: {
- username: "username1",
- password: "password1",
- fido2Credentials: [fido2Credential],
- },
- });
- overlayBackground["inlineMenuCiphers"] = new Map([["inline-menu-cipher-1", cipher1]]);
- const pageDetailsForTab = {
- frameId: sender.frameId,
- tab: sender.tab,
- details: pageDetails,
- };
- overlayBackground["pageDetailsForTab"][sender.tab.id] = new Map([
- [sender.frameId, pageDetailsForTab],
- ]);
- autofillService.isPasswordRepromptRequired.mockResolvedValue(false);
- jest.spyOn(fido2ActiveRequestManager, "getActiveRequest");
+ describe("triggering passkey authentication", () => {
+ let cipher1: CipherView;
- sendPortMessage(listMessageConnectorSpy, {
- command: "fillAutofillInlineMenuCipher",
- inlineMenuCipherId: "inline-menu-cipher-1",
- usePasskey: true,
- portKey,
+ beforeEach(() => {
+ const fido2Credential = mock({ credentialId: "credential-id" });
+ cipher1 = mock({
+ id: "inline-menu-cipher-1",
+ login: {
+ username: "username1",
+ password: "password1",
+ fido2Credentials: [fido2Credential],
+ },
+ });
+ const pageDetailsForTab = {
+ frameId: sender.frameId,
+ tab: sender.tab,
+ details: pageDetails,
+ };
+ overlayBackground["inlineMenuCiphers"] = new Map([["inline-menu-cipher-1", cipher1]]);
+ overlayBackground["pageDetailsForTab"][sender.tab.id] = new Map([
+ [sender.frameId, pageDetailsForTab],
+ ]);
+ autofillService.isPasswordRepromptRequired.mockResolvedValue(false);
});
- await flushPromises();
- expect(fido2ActiveRequestManager.getActiveRequest).toHaveBeenCalledWith(sender.tab.id);
+ it("logs an error if the authentication could not complete due to a missing FIDO2 request", async () => {
+ jest.spyOn(logService, "error");
+
+ sendPortMessage(listMessageConnectorSpy, {
+ command: "fillAutofillInlineMenuCipher",
+ inlineMenuCipherId: "inline-menu-cipher-1",
+ usePasskey: true,
+ portKey,
+ });
+ await flushPromises();
+
+ expect(logService.error).toHaveBeenCalled();
+ });
+
+ describe("when the FIDO2 request is present", () => {
+ beforeEach(async () => {
+ void fido2ActiveRequestManager.newActiveRequest(
+ sender.tab.id,
+ cipher1.login.fido2Credentials,
+ new AbortController(),
+ );
+ });
+
+ it("aborts all active FIDO2 requests if the subsequent request after the authentication is invalid", async () => {
+ jest.spyOn(fido2ActiveRequestManager, "removeActiveRequest");
+
+ sendPortMessage(listMessageConnectorSpy, {
+ command: "fillAutofillInlineMenuCipher",
+ inlineMenuCipherId: "inline-menu-cipher-1",
+ usePasskey: true,
+ portKey,
+ });
+ await flushPromises();
+ triggerWebRequestOnCompletedEvent(
+ mock({
+ statusCode: 401,
+ }),
+ );
+
+ expect(fido2ActiveRequestManager.removeActiveRequest).toHaveBeenCalled();
+ });
+
+ it("triggers a closure of the inline menu if the subsequent request after the authentication is valid", async () => {
+ jest.useFakeTimers();
+
+ await initOverlayElementPorts();
+ sendPortMessage(listMessageConnectorSpy, {
+ command: "fillAutofillInlineMenuCipher",
+ inlineMenuCipherId: "inline-menu-cipher-1",
+ usePasskey: true,
+ portKey,
+ });
+ triggerWebRequestOnCompletedEvent(
+ mock({
+ statusCode: 200,
+ }),
+ );
+ jest.advanceTimersByTime(3100);
+
+ expect(listPortSpy.postMessage).toHaveBeenCalledWith({
+ command: "triggerDelayedAutofillInlineMenuClosure",
+ });
+ });
+ });
});
});
diff --git a/apps/browser/src/autofill/background/overlay.background.ts b/apps/browser/src/autofill/background/overlay.background.ts
index c8d250df50..653d31ca52 100644
--- a/apps/browser/src/autofill/background/overlay.background.ts
+++ b/apps/browser/src/autofill/background/overlay.background.ts
@@ -55,7 +55,11 @@ import {
MAX_SUB_FRAME_DEPTH,
} from "../enums/autofill-overlay.enum";
import { AutofillService } from "../services/abstractions/autofill.service";
-import { generateRandomChars } from "../utils";
+import {
+ generateDomainMatchPatterns,
+ generateRandomChars,
+ isInvalidResponseStatusCode,
+} from "../utils";
import { LockedVaultPendingNotificationsData } from "./abstractions/notification.background";
import {
@@ -151,7 +155,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
addEditCipherSubmitted: () => this.updateOverlayCiphers(),
editedCipher: () => this.updateOverlayCiphers(),
deletedCipher: () => this.updateOverlayCiphers(),
- fido2AbortRequest: ({ sender }) => this.abortFido2ActiveRequest(sender),
+ fido2AbortRequest: ({ sender }) => this.abortFido2ActiveRequest(sender.tab.id),
};
private readonly inlineMenuButtonPortMessageHandlers: InlineMenuButtonPortMessageHandlers = {
triggerDelayedAutofillInlineMenuClosure: () => this.triggerDelayedInlineMenuClosure(),
@@ -672,10 +676,10 @@ export class OverlayBackground implements OverlayBackgroundInterface {
/**
* Aborts an active FIDO2 request for a given tab and updates the inline menu ciphers.
*
- * @param sender - The sender of the message
+ * @param tabId - The id of the tab to abort the request for
*/
- private async abortFido2ActiveRequest(sender: chrome.runtime.MessageSender) {
- this.fido2ActiveRequestManager.removeActiveRequest(sender.tab.id);
+ private async abortFido2ActiveRequest(tabId: number) {
+ this.fido2ActiveRequestManager.removeActiveRequest(tabId);
await this.updateOverlayCiphers(false);
}
@@ -939,11 +943,10 @@ export class OverlayBackground implements OverlayBackgroundInterface {
if (usePasskey && cipher.login?.hasFido2Credentials) {
await this.authenticatePasskeyCredential(
- sender.tab.id,
+ sender,
cipher.login.fido2Credentials[0].credentialId,
);
this.updateLastUsedInlineMenuCipher(inlineMenuCipherId, cipher);
- this.closeInlineMenu(sender, { forceCloseInlineMenu: true });
return;
}
@@ -969,11 +972,11 @@ export class OverlayBackground implements OverlayBackgroundInterface {
/**
* Triggers a FIDO2 authentication from the inline menu using the passed credential ID.
*
- * @param tabId - The tab ID to trigger the authentication for
+ * @param sender - The sender of the port message
* @param credentialId - The credential ID to authenticate
*/
- async authenticatePasskeyCredential(tabId: number, credentialId: string) {
- const request = this.fido2ActiveRequestManager.getActiveRequest(tabId);
+ async authenticatePasskeyCredential(sender: chrome.runtime.MessageSender, credentialId: string) {
+ const request = this.fido2ActiveRequestManager.getActiveRequest(sender.tab.id);
if (!request) {
this.logService.error(
"Could not complete passkey autofill due to missing active Fido2 request",
@@ -981,9 +984,35 @@ export class OverlayBackground implements OverlayBackgroundInterface {
return;
}
+ chrome.webRequest.onCompleted.addListener(this.handlePasskeyAuthenticationOnCompleted, {
+ urls: generateDomainMatchPatterns(sender.tab.url),
+ });
request.subject.next({ type: Fido2ActiveRequestEvents.Continue, credentialId });
}
+ /**
+ * Handles the next web request that occurs after a passkey authentication has been completed.
+ * Ensures that the inline menu closes after the request, and that the FIDO2 request is aborted
+ * if the request is not successful.
+ *
+ * @param details - The web request details
+ */
+ private handlePasskeyAuthenticationOnCompleted = (
+ details: chrome.webRequest.WebResponseCacheDetails,
+ ) => {
+ chrome.webRequest.onCompleted.removeListener(this.handlePasskeyAuthenticationOnCompleted);
+
+ if (isInvalidResponseStatusCode(details.statusCode)) {
+ this.closeInlineMenu({ tab: { id: details.tabId } } as chrome.runtime.MessageSender, {
+ forceCloseInlineMenu: true,
+ });
+ this.abortFido2ActiveRequest(details.tabId).catch((error) => this.logService.error(error));
+ return;
+ }
+
+ globalThis.setTimeout(() => this.triggerDelayedInlineMenuClosure(), 3000);
+ };
+
/**
* Sets the most recently used cipher at the top of the list of ciphers.
*
@@ -1587,6 +1616,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
passkeys: this.i18nService.translate("passkeys"),
passwords: this.i18nService.translate("passwords"),
logInWithPasskey: this.i18nService.translate("logInWithPasskeyAriaLabel"),
+ authenticating: this.i18nService.translate("authenticating"),
};
}
diff --git a/apps/browser/src/autofill/overlay/inline-menu/pages/list/__snapshots__/autofill-inline-menu-list.spec.ts.snap b/apps/browser/src/autofill/overlay/inline-menu/pages/list/__snapshots__/autofill-inline-menu-list.spec.ts.snap
index 3339781fab..0a4ae8d795 100644
--- a/apps/browser/src/autofill/overlay/inline-menu/pages/list/__snapshots__/autofill-inline-menu-list.spec.ts.snap
+++ b/apps/browser/src/autofill/overlay/inline-menu/pages/list/__snapshots__/autofill-inline-menu-list.spec.ts.snap
@@ -2131,6 +2131,44 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers f
`;
+exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers for an authenticated user fill cipher button event listeners filling a cipher displays an \`Authenticating\` loader when a passkey cipher is filled 1`] = `
+
+`;
+
exports[`AutofillInlineMenuList initAutofillInlineMenuList the locked inline menu for an unauthenticated user creates the views for the locked inline menu 1`] = `