mirror of
https://github.com/bitwarden/browser
synced 2025-12-06 00:13:28 +00:00
[PM-21005] Clear Add/Edit form cache when browser loses focus (#14634)
This commit is contained in:
@@ -81,6 +81,7 @@ export class PopupViewCacheService implements ViewCacheService {
|
||||
injector = inject(Injector),
|
||||
initialValue,
|
||||
persistNavigation,
|
||||
clearOnTabChange,
|
||||
} = options;
|
||||
const cachedValue = this.cache[key]?.value
|
||||
? deserializer(JSON.parse(this.cache[key].value))
|
||||
@@ -89,6 +90,7 @@ export class PopupViewCacheService implements ViewCacheService {
|
||||
|
||||
const viewCacheOptions = {
|
||||
...(persistNavigation && { persistNavigation }),
|
||||
...(clearOnTabChange && { clearOnTabChange }),
|
||||
};
|
||||
|
||||
effect(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { switchMap, delay, filter, concatMap } from "rxjs";
|
||||
import { switchMap, delay, filter, concatMap, map, first, of } from "rxjs";
|
||||
|
||||
import { CommandDefinition, MessageListener } from "@bitwarden/common/platform/messaging";
|
||||
import {
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
GlobalStateProvider,
|
||||
} from "@bitwarden/common/platform/state";
|
||||
|
||||
import { BrowserApi } from "../browser/browser-api";
|
||||
import { fromChromeEvent } from "../browser/from-chrome-event";
|
||||
|
||||
const popupClosedPortName = "new_popup";
|
||||
@@ -21,6 +22,12 @@ export type ViewCacheOptions = {
|
||||
* Optional flag to persist the cached value between navigation events.
|
||||
*/
|
||||
persistNavigation?: boolean;
|
||||
|
||||
/**
|
||||
* When set, the cached value will be cleared when the user changes tabs.
|
||||
* @optional
|
||||
*/
|
||||
clearOnTabChange?: true;
|
||||
};
|
||||
|
||||
export type ViewCacheState = {
|
||||
@@ -129,6 +136,37 @@ export class PopupViewCacheBackgroundService {
|
||||
),
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
// On tab changed, excluding extension tabs
|
||||
fromChromeEvent(chrome.tabs.onActivated)
|
||||
.pipe(
|
||||
switchMap((tabs) => BrowserApi.getTab(tabs[0].tabId)!),
|
||||
switchMap((tab) => {
|
||||
// FireFox sets the `url` to "about:blank" and won't populate the `url` until the `onUpdated` event
|
||||
if (tab.url !== "about:blank") {
|
||||
return of(tab);
|
||||
}
|
||||
|
||||
return fromChromeEvent(chrome.tabs.onUpdated).pipe(
|
||||
first(),
|
||||
switchMap(([tabId]) => BrowserApi.getTab(tabId)!),
|
||||
);
|
||||
}),
|
||||
map((tab) => tab.url || tab.pendingUrl),
|
||||
filter((url) => !url?.startsWith(chrome.runtime.getURL(""))),
|
||||
switchMap(() =>
|
||||
this.popupViewCacheState.update((state) => {
|
||||
if (!state) {
|
||||
return null;
|
||||
}
|
||||
// Only remove keys that are marked with `clearOnTabChange`
|
||||
return Object.fromEntries(
|
||||
Object.entries(state).filter(([, { options }]) => !options?.clearOnTabChange),
|
||||
);
|
||||
}),
|
||||
),
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
async clearState() {
|
||||
|
||||
@@ -14,6 +14,7 @@ import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { EventType } from "@bitwarden/common/enums";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { CipherId, CollectionId, OrganizationId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
@@ -40,6 +41,7 @@ import {
|
||||
} from "@bitwarden/vault";
|
||||
|
||||
import { BrowserFido2UserInterfaceSession } from "../../../../../autofill/fido2/services/browser-fido2-user-interface.service";
|
||||
import { BrowserApi } from "../../../../../platform/browser/browser-api";
|
||||
import BrowserPopupUtils from "../../../../../platform/popup/browser-popup-utils";
|
||||
import { PopOutComponent } from "../../../../../platform/popup/components/pop-out.component";
|
||||
import { PopupFooterComponent } from "../../../../../platform/popup/layout/popup-footer.component";
|
||||
@@ -70,6 +72,7 @@ class QueryParams {
|
||||
this.uri = params.uri;
|
||||
this.username = params.username;
|
||||
this.name = params.name;
|
||||
this.prefillNameAndURIFromTab = params.prefillNameAndURIFromTab;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -116,6 +119,12 @@ class QueryParams {
|
||||
* Optional name to pre-fill for the cipher.
|
||||
*/
|
||||
name?: string;
|
||||
|
||||
/**
|
||||
* Optional flag to pre-fill the name and URI from the current tab.
|
||||
* NOTE: This will override the `uri` and `name` query parameters if set to true.
|
||||
*/
|
||||
prefillNameAndURIFromTab?: true;
|
||||
}
|
||||
|
||||
export type AddEditQueryParams = Partial<Record<keyof QueryParams, string>>;
|
||||
@@ -281,8 +290,7 @@ export class AddEditV2Component implements OnInit {
|
||||
if (config.mode === "edit" && !config.originalCipher.edit) {
|
||||
config.mode = "partial-edit";
|
||||
}
|
||||
|
||||
config.initialValues = this.setInitialValuesFromParams(params);
|
||||
config.initialValues = await this.setInitialValuesFromParams(params);
|
||||
|
||||
const activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(getUserId),
|
||||
@@ -326,7 +334,7 @@ export class AddEditV2Component implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
setInitialValuesFromParams(params: QueryParams) {
|
||||
async setInitialValuesFromParams(params: QueryParams) {
|
||||
const initialValues = {} as OptionalInitialValues;
|
||||
if (params.folderId) {
|
||||
initialValues.folderId = params.folderId;
|
||||
@@ -346,6 +354,14 @@ export class AddEditV2Component implements OnInit {
|
||||
if (params.name) {
|
||||
initialValues.name = params.name;
|
||||
}
|
||||
|
||||
if (params.prefillNameAndURIFromTab) {
|
||||
const tab = await BrowserApi.getTabFromCurrentWindow();
|
||||
|
||||
initialValues.loginUri = tab.url;
|
||||
initialValues.name = Utils.getHostname(tab.url);
|
||||
}
|
||||
|
||||
return initialValues;
|
||||
}
|
||||
|
||||
|
||||
@@ -94,8 +94,7 @@ describe("NewItemDropdownV2Component", () => {
|
||||
collectionId: "777-888-999",
|
||||
organizationId: "444-555-666",
|
||||
folderId: "222-333-444",
|
||||
uri: "https://example.com",
|
||||
name: "example.com",
|
||||
prefillNameAndURIFromTab: "true",
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -2,10 +2,9 @@
|
||||
// @ts-strict-ignore
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, Input, OnInit } from "@angular/core";
|
||||
import { Router, RouterLink } from "@angular/router";
|
||||
import { RouterLink } from "@angular/router";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { ButtonModule, DialogService, MenuModule, NoItemsModule } from "@bitwarden/components";
|
||||
@@ -35,10 +34,8 @@ export class NewItemDropdownV2Component implements OnInit {
|
||||
*/
|
||||
@Input()
|
||||
initialValues: NewItemInitialValues;
|
||||
constructor(
|
||||
private router: Router,
|
||||
private dialogService: DialogService,
|
||||
) {}
|
||||
|
||||
constructor(private dialogService: DialogService) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.tab = await BrowserApi.getTabFromCurrentWindow();
|
||||
@@ -47,13 +44,12 @@ export class NewItemDropdownV2Component implements OnInit {
|
||||
buildQueryParams(type: CipherType): AddEditQueryParams {
|
||||
const poppedOut = BrowserPopupUtils.inPopout(window);
|
||||
|
||||
const loginDetails: { uri?: string; name?: string } = {};
|
||||
const loginDetails: { prefillNameAndURIFromTab?: string } = {};
|
||||
|
||||
// When a Login Cipher is created and the extension is not popped out,
|
||||
// pass along the uri and name
|
||||
if (!poppedOut && type === CipherType.Login && this.tab) {
|
||||
loginDetails.uri = this.tab.url;
|
||||
loginDetails.name = Utils.getHostname(this.tab.url);
|
||||
loginDetails.prefillNameAndURIFromTab = "true";
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -23,6 +23,12 @@ type BaseCacheOptions<T> = {
|
||||
* Optional flag to persist the cached value between navigation events.
|
||||
*/
|
||||
persistNavigation?: boolean;
|
||||
|
||||
/**
|
||||
* When set, the cached value will be cleared when the user changes tabs.
|
||||
* @optional
|
||||
*/
|
||||
clearOnTabChange?: true;
|
||||
} & (T extends JsonValue ? Deserializer<T> : Required<Deserializer<T>>);
|
||||
|
||||
export type SignalCacheOptions<T> = BaseCacheOptions<T> & {
|
||||
|
||||
@@ -27,6 +27,7 @@ export class CipherFormCacheService {
|
||||
key: CIPHER_FORM_CACHE_KEY,
|
||||
initialValue: null,
|
||||
deserializer: CipherView.fromJSON,
|
||||
clearOnTabChange: true,
|
||||
});
|
||||
|
||||
constructor() {
|
||||
|
||||
Reference in New Issue
Block a user