mirror of
https://github.com/bitwarden/browser
synced 2025-12-11 22:03:36 +00:00
[PM-3530][PM-8588] persist extension route history (#9556)
Save the extension popup route history and restore it after closing and re-opening the popup. --------- Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com>
This commit is contained in:
@@ -28,6 +28,7 @@ import { UserId } from "@bitwarden/common/types/guid";
|
|||||||
import { ButtonModule, I18nMockService } from "@bitwarden/components";
|
import { ButtonModule, I18nMockService } from "@bitwarden/components";
|
||||||
|
|
||||||
import { RegistrationCheckEmailIcon } from "../../../../../../libs/auth/src/angular/icons/registration-check-email.icon";
|
import { RegistrationCheckEmailIcon } from "../../../../../../libs/auth/src/angular/icons/registration-check-email.icon";
|
||||||
|
import { PopupRouterCacheService } from "../../../platform/popup/view-cache/popup-router-cache.service";
|
||||||
|
|
||||||
import { ExtensionAnonLayoutWrapperDataService } from "./extension-anon-layout-wrapper-data.service";
|
import { ExtensionAnonLayoutWrapperDataService } from "./extension-anon-layout-wrapper-data.service";
|
||||||
import {
|
import {
|
||||||
@@ -145,7 +146,15 @@ const decorators = (options: {
|
|||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
applicationConfig({
|
applicationConfig({
|
||||||
providers: [importProvidersFrom(RouterModule.forRoot(options.routes))],
|
providers: [
|
||||||
|
importProvidersFrom(RouterModule.forRoot(options.routes)),
|
||||||
|
{
|
||||||
|
provide: PopupRouterCacheService,
|
||||||
|
useValue: {
|
||||||
|
back() {},
|
||||||
|
} as Partial<PopupRouterCacheService>,
|
||||||
|
},
|
||||||
|
],
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -236,6 +236,7 @@ import I18nService from "../platform/services/i18n.service";
|
|||||||
import { LocalBackedSessionStorageService } from "../platform/services/local-backed-session-storage.service";
|
import { LocalBackedSessionStorageService } from "../platform/services/local-backed-session-storage.service";
|
||||||
import { BackgroundPlatformUtilsService } from "../platform/services/platform-utils/background-platform-utils.service";
|
import { BackgroundPlatformUtilsService } from "../platform/services/platform-utils/background-platform-utils.service";
|
||||||
import { BrowserPlatformUtilsService } from "../platform/services/platform-utils/browser-platform-utils.service";
|
import { BrowserPlatformUtilsService } from "../platform/services/platform-utils/browser-platform-utils.service";
|
||||||
|
import { PopupViewCacheBackgroundService } from "../platform/services/popup-view-cache-background.service";
|
||||||
import { BackgroundTaskSchedulerService } from "../platform/services/task-scheduler/background-task-scheduler.service";
|
import { BackgroundTaskSchedulerService } from "../platform/services/task-scheduler/background-task-scheduler.service";
|
||||||
import { ForegroundTaskSchedulerService } from "../platform/services/task-scheduler/foreground-task-scheduler.service";
|
import { ForegroundTaskSchedulerService } from "../platform/services/task-scheduler/foreground-task-scheduler.service";
|
||||||
import { BackgroundMemoryStorageService } from "../platform/storage/background-memory-storage.service";
|
import { BackgroundMemoryStorageService } from "../platform/storage/background-memory-storage.service";
|
||||||
@@ -371,6 +372,8 @@ export default class MainBackground {
|
|||||||
private isSafari: boolean;
|
private isSafari: boolean;
|
||||||
private nativeMessagingBackground: NativeMessagingBackground;
|
private nativeMessagingBackground: NativeMessagingBackground;
|
||||||
|
|
||||||
|
private popupViewCacheBackgroundService: PopupViewCacheBackgroundService;
|
||||||
|
|
||||||
constructor(public popupOnlyContext: boolean = false) {
|
constructor(public popupOnlyContext: boolean = false) {
|
||||||
// Services
|
// Services
|
||||||
const lockedCallback = async (userId?: string) => {
|
const lockedCallback = async (userId?: string) => {
|
||||||
@@ -571,6 +574,10 @@ export default class MainBackground {
|
|||||||
logoutCallback,
|
logoutCallback,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.popupViewCacheBackgroundService = new PopupViewCacheBackgroundService(
|
||||||
|
this.globalStateProvider,
|
||||||
|
);
|
||||||
|
|
||||||
const migrationRunner = new MigrationRunner(
|
const migrationRunner = new MigrationRunner(
|
||||||
this.storageService,
|
this.storageService,
|
||||||
this.logService,
|
this.logService,
|
||||||
@@ -1201,6 +1208,8 @@ export default class MainBackground {
|
|||||||
await (this.i18nService as I18nService).init();
|
await (this.i18nService as I18nService).init();
|
||||||
(this.eventUploadService as EventUploadService).init(true);
|
(this.eventUploadService as EventUploadService).init(true);
|
||||||
|
|
||||||
|
this.popupViewCacheBackgroundService.startObservingTabChanges();
|
||||||
|
|
||||||
if (this.popupOnlyContext) {
|
if (this.popupOnlyContext) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1295,6 +1304,7 @@ export default class MainBackground {
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
await this.popupViewCacheBackgroundService.clearState();
|
||||||
await this.accountService.switchAccount(userId);
|
await this.accountService.switchAccount(userId);
|
||||||
await switchPromise;
|
await switchPromise;
|
||||||
// Clear sequentialized caches
|
// Clear sequentialized caches
|
||||||
@@ -1383,6 +1393,7 @@ export default class MainBackground {
|
|||||||
this.vaultTimeoutSettingsService.clear(userBeingLoggedOut),
|
this.vaultTimeoutSettingsService.clear(userBeingLoggedOut),
|
||||||
this.vaultFilterService.clear(),
|
this.vaultFilterService.clear(),
|
||||||
this.biometricStateService.logout(userBeingLoggedOut),
|
this.biometricStateService.logout(userBeingLoggedOut),
|
||||||
|
this.popupViewCacheBackgroundService.clearState(),
|
||||||
/* We intentionally do not clear:
|
/* We intentionally do not clear:
|
||||||
* - autofillSettingsService
|
* - autofillSettingsService
|
||||||
* - badgeSettingsService
|
* - badgeSettingsService
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { BooleanInput, coerceBooleanProperty } from "@angular/cdk/coercion";
|
import { BooleanInput, coerceBooleanProperty } from "@angular/cdk/coercion";
|
||||||
import { CommonModule, Location } from "@angular/common";
|
import { CommonModule } from "@angular/common";
|
||||||
import { Component, Input, Signal, inject } from "@angular/core";
|
import { Component, Input, Signal, inject } from "@angular/core";
|
||||||
|
|
||||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
@@ -10,6 +10,8 @@ import {
|
|||||||
TypographyModule,
|
TypographyModule,
|
||||||
} from "@bitwarden/components";
|
} from "@bitwarden/components";
|
||||||
|
|
||||||
|
import { PopupRouterCacheService } from "../view-cache/popup-router-cache.service";
|
||||||
|
|
||||||
import { PopupPageComponent } from "./popup-page.component";
|
import { PopupPageComponent } from "./popup-page.component";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -19,6 +21,7 @@ import { PopupPageComponent } from "./popup-page.component";
|
|||||||
imports: [TypographyModule, CommonModule, IconButtonModule, JslibModule, AsyncActionsModule],
|
imports: [TypographyModule, CommonModule, IconButtonModule, JslibModule, AsyncActionsModule],
|
||||||
})
|
})
|
||||||
export class PopupHeaderComponent {
|
export class PopupHeaderComponent {
|
||||||
|
private popupRouterCacheService = inject(PopupRouterCacheService);
|
||||||
protected pageContentScrolled: Signal<boolean> = inject(PopupPageComponent).isScrolled;
|
protected pageContentScrolled: Signal<boolean> = inject(PopupPageComponent).isScrolled;
|
||||||
|
|
||||||
/** Background color */
|
/** Background color */
|
||||||
@@ -46,8 +49,6 @@ export class PopupHeaderComponent {
|
|||||||
**/
|
**/
|
||||||
@Input()
|
@Input()
|
||||||
backAction: FunctionReturningAwaitable = async () => {
|
backAction: FunctionReturningAwaitable = async () => {
|
||||||
this.location.back();
|
return this.popupRouterCacheService.back();
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(private location: Location) {}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ import {
|
|||||||
SectionComponent,
|
SectionComponent,
|
||||||
} from "@bitwarden/components";
|
} from "@bitwarden/components";
|
||||||
|
|
||||||
|
import { PopupRouterCacheService } from "../view-cache/popup-router-cache.service";
|
||||||
|
|
||||||
import { PopupFooterComponent } from "./popup-footer.component";
|
import { PopupFooterComponent } from "./popup-footer.component";
|
||||||
import { PopupHeaderComponent } from "./popup-header.component";
|
import { PopupHeaderComponent } from "./popup-header.component";
|
||||||
import { PopupPageComponent } from "./popup-page.component";
|
import { PopupPageComponent } from "./popup-page.component";
|
||||||
@@ -334,6 +336,12 @@ export default {
|
|||||||
{ useHash: true },
|
{ useHash: true },
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
{
|
||||||
|
provide: PopupRouterCacheService,
|
||||||
|
useValue: {
|
||||||
|
back() {},
|
||||||
|
} as Partial<PopupRouterCacheService>,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -0,0 +1,131 @@
|
|||||||
|
import { Location } from "@angular/common";
|
||||||
|
import { Injectable, inject } from "@angular/core";
|
||||||
|
import {
|
||||||
|
ActivatedRouteSnapshot,
|
||||||
|
CanActivateFn,
|
||||||
|
NavigationEnd,
|
||||||
|
Router,
|
||||||
|
UrlSerializer,
|
||||||
|
} from "@angular/router";
|
||||||
|
import { filter, first, firstValueFrom, map, Observable, of, switchMap } from "rxjs";
|
||||||
|
|
||||||
|
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||||
|
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||||
|
import { GlobalStateProvider } from "@bitwarden/common/platform/state";
|
||||||
|
|
||||||
|
import { POPUP_ROUTE_HISTORY_KEY } from "../../../platform/services/popup-view-cache-background.service";
|
||||||
|
import BrowserPopupUtils from "../browser-popup-utils";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Preserves route history when opening and closing the popup
|
||||||
|
*
|
||||||
|
* Routes marked with `doNotSaveUrl` will not be stored
|
||||||
|
**/
|
||||||
|
@Injectable({
|
||||||
|
providedIn: "root",
|
||||||
|
})
|
||||||
|
export class PopupRouterCacheService {
|
||||||
|
private router = inject(Router);
|
||||||
|
private state = inject(GlobalStateProvider).get(POPUP_ROUTE_HISTORY_KEY);
|
||||||
|
private location = inject(Location);
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
// init history with existing state
|
||||||
|
this.history$()
|
||||||
|
.pipe(first())
|
||||||
|
.subscribe((history) => history.forEach((location) => this.location.go(location)));
|
||||||
|
|
||||||
|
// update state when route change occurs
|
||||||
|
this.router.events
|
||||||
|
.pipe(
|
||||||
|
filter((event) => event instanceof NavigationEnd),
|
||||||
|
filter((_event: NavigationEnd) => {
|
||||||
|
const state: ActivatedRouteSnapshot = this.router.routerState.snapshot.root;
|
||||||
|
|
||||||
|
let child = state.firstChild;
|
||||||
|
while (child.firstChild) {
|
||||||
|
child = child.firstChild;
|
||||||
|
}
|
||||||
|
|
||||||
|
return !child?.data?.doNotSaveUrl ?? true;
|
||||||
|
}),
|
||||||
|
switchMap((event) => this.push(event.url)),
|
||||||
|
)
|
||||||
|
.subscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
history$(): Observable<string[]> {
|
||||||
|
return this.state.state$;
|
||||||
|
}
|
||||||
|
|
||||||
|
async setHistory(state: string[]): Promise<string[]> {
|
||||||
|
return this.state.update(() => state);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get the last item from the history stack, or `null` if empty */
|
||||||
|
last$(): Observable<string | null> {
|
||||||
|
return this.history$().pipe(
|
||||||
|
map((history) => {
|
||||||
|
if (!history || history.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return history[history.length - 1];
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If in browser popup, push new route onto history stack
|
||||||
|
*/
|
||||||
|
private async push(url: string): Promise<boolean> {
|
||||||
|
if (!BrowserPopupUtils.inPopup(window) || url === (await firstValueFrom(this.last$()))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await this.state.update((prevState) => (prevState == null ? [url] : prevState.concat(url)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigate back in history
|
||||||
|
*/
|
||||||
|
async back() {
|
||||||
|
await this.state.update((prevState) => prevState.slice(0, -1));
|
||||||
|
|
||||||
|
const url = this.router.url;
|
||||||
|
this.location.back();
|
||||||
|
if (url !== this.router.url) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if no history is present, fallback to vault page
|
||||||
|
await this.router.navigate([""]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redirect to the last visited route. Should be applied to root route.
|
||||||
|
*
|
||||||
|
* If `FeatureFlag.PersistPopupView` is disabled, do nothing.
|
||||||
|
**/
|
||||||
|
export const popupRouterCacheGuard = (() => {
|
||||||
|
const configService = inject(ConfigService);
|
||||||
|
const popupHistoryService = inject(PopupRouterCacheService);
|
||||||
|
const urlSerializer = inject(UrlSerializer);
|
||||||
|
|
||||||
|
return configService.getFeatureFlag$(FeatureFlag.PersistPopupView).pipe(
|
||||||
|
switchMap((featureEnabled) => {
|
||||||
|
if (!featureEnabled) {
|
||||||
|
return of(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return popupHistoryService.last$().pipe(
|
||||||
|
map((url: string) => {
|
||||||
|
if (!url) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return urlSerializer.parse(url);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}) satisfies CanActivateFn;
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
import { Component } from "@angular/core";
|
||||||
|
import { TestBed } from "@angular/core/testing";
|
||||||
|
import { Router, UrlSerializer, UrlTree } from "@angular/router";
|
||||||
|
import { RouterTestingModule } from "@angular/router/testing";
|
||||||
|
import { mock } from "jest-mock-extended";
|
||||||
|
import { firstValueFrom, of } from "rxjs";
|
||||||
|
|
||||||
|
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||||
|
import { GlobalStateProvider } from "@bitwarden/common/platform/state";
|
||||||
|
import { FakeGlobalStateProvider } from "@bitwarden/common/spec";
|
||||||
|
|
||||||
|
import { PopupRouterCacheService, popupRouterCacheGuard } from "./popup-router-cache.service";
|
||||||
|
|
||||||
|
const flushPromises = async () => await new Promise(process.nextTick);
|
||||||
|
|
||||||
|
@Component({ template: "" })
|
||||||
|
export class EmptyComponent {}
|
||||||
|
|
||||||
|
describe("Popup router cache guard", () => {
|
||||||
|
const configServiceMock = mock<ConfigService>();
|
||||||
|
const fakeGlobalStateProvider = new FakeGlobalStateProvider();
|
||||||
|
|
||||||
|
let testBed: TestBed;
|
||||||
|
let serializer: UrlSerializer;
|
||||||
|
let router: Router;
|
||||||
|
|
||||||
|
let service: PopupRouterCacheService;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
jest.spyOn(configServiceMock, "getFeatureFlag$").mockReturnValue(of(true));
|
||||||
|
|
||||||
|
testBed = TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
RouterTestingModule.withRoutes([
|
||||||
|
{ path: "a", component: EmptyComponent },
|
||||||
|
{ path: "b", component: EmptyComponent },
|
||||||
|
{
|
||||||
|
path: "c",
|
||||||
|
component: EmptyComponent,
|
||||||
|
data: { doNotSaveUrl: true },
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
{ provide: ConfigService, useValue: configServiceMock },
|
||||||
|
{ provide: GlobalStateProvider, useValue: fakeGlobalStateProvider },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
await testBed.compileComponents();
|
||||||
|
|
||||||
|
router = testBed.inject(Router);
|
||||||
|
serializer = testBed.inject(UrlSerializer);
|
||||||
|
|
||||||
|
service = testBed.inject(PopupRouterCacheService);
|
||||||
|
|
||||||
|
await service.setHistory([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns true if the history stack is empty", async () => {
|
||||||
|
const response = await firstValueFrom(
|
||||||
|
testBed.runInInjectionContext(() => popupRouterCacheGuard()),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(response).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("redirects to the latest stored route", async () => {
|
||||||
|
await router.navigate(["a"]);
|
||||||
|
await router.navigate(["b"]);
|
||||||
|
|
||||||
|
const response = (await firstValueFrom(
|
||||||
|
testBed.runInInjectionContext(() => popupRouterCacheGuard()),
|
||||||
|
)) as UrlTree;
|
||||||
|
|
||||||
|
expect(serializer.serialize(response)).toBe("/b");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("back method redirects to the previous route", async () => {
|
||||||
|
await router.navigate(["a"]);
|
||||||
|
await router.navigate(["b"]);
|
||||||
|
|
||||||
|
// wait for router events subscription
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(await firstValueFrom(service.history$())).toEqual(["/a", "/b"]);
|
||||||
|
|
||||||
|
await service.back();
|
||||||
|
|
||||||
|
expect(await firstValueFrom(service.history$())).toEqual(["/a"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not save ignored routes", async () => {
|
||||||
|
await router.navigate(["a"]);
|
||||||
|
await router.navigate(["b"]);
|
||||||
|
await router.navigate(["c"]);
|
||||||
|
|
||||||
|
const response = (await firstValueFrom(
|
||||||
|
testBed.runInInjectionContext(() => popupRouterCacheGuard()),
|
||||||
|
)) as UrlTree;
|
||||||
|
|
||||||
|
expect(serializer.serialize(response)).toBe("/b");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not save duplicate routes", async () => {
|
||||||
|
await router.navigate(["a"]);
|
||||||
|
await router.navigate(["a"]);
|
||||||
|
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(await firstValueFrom(service.history$())).toEqual(["/a"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import { switchMap, merge, delay, filter, map } from "rxjs";
|
||||||
|
|
||||||
|
import {
|
||||||
|
POPUP_VIEW_MEMORY,
|
||||||
|
KeyDefinition,
|
||||||
|
GlobalStateProvider,
|
||||||
|
} from "@bitwarden/common/platform/state";
|
||||||
|
|
||||||
|
import { fromChromeEvent } from "../browser/from-chrome-event";
|
||||||
|
|
||||||
|
const popupClosedPortName = "new_popup";
|
||||||
|
|
||||||
|
export const POPUP_ROUTE_HISTORY_KEY = new KeyDefinition<string[]>(
|
||||||
|
POPUP_VIEW_MEMORY,
|
||||||
|
"popup-route-history",
|
||||||
|
{
|
||||||
|
deserializer: (jsonValue) => jsonValue,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export class PopupViewCacheBackgroundService {
|
||||||
|
private popupRouteHistoryState = this.globalStateProvider.get(POPUP_ROUTE_HISTORY_KEY);
|
||||||
|
|
||||||
|
constructor(private globalStateProvider: GlobalStateProvider) {}
|
||||||
|
|
||||||
|
startObservingTabChanges() {
|
||||||
|
merge(
|
||||||
|
// on tab changed, excluding extension tabs
|
||||||
|
fromChromeEvent(chrome.tabs.onActivated).pipe(
|
||||||
|
switchMap(([tabInfo]) => chrome.tabs.get(tabInfo.tabId)),
|
||||||
|
map((tab) => tab.url || tab.pendingUrl),
|
||||||
|
filter((url) => !url.startsWith(chrome.runtime.getURL(""))),
|
||||||
|
),
|
||||||
|
|
||||||
|
// on popup closed, with 2 minute delay that is cancelled by re-opening the popup
|
||||||
|
fromChromeEvent(chrome.runtime.onConnect).pipe(
|
||||||
|
filter(([port]) => port.name === popupClosedPortName),
|
||||||
|
switchMap(([port]) => fromChromeEvent(port.onDisconnect).pipe(delay(1000 * 60 * 2))),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.pipe(switchMap(() => this.clearState()))
|
||||||
|
.subscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
async clearState() {
|
||||||
|
return Promise.all([
|
||||||
|
this.popupRouteHistoryState.update(() => [], { shouldUpdate: this.objNotEmpty }),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private objNotEmpty(obj: object): boolean {
|
||||||
|
return Object.keys(obj ?? {}).length !== 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Communicates to {@link PopupViewCacheBackgroundService} that the extension popup has been closed.
|
||||||
|
*
|
||||||
|
* Call in the foreground.
|
||||||
|
**/
|
||||||
|
export const initPopupClosedListener = () => {
|
||||||
|
chrome.runtime.connect({ name: popupClosedPortName });
|
||||||
|
};
|
||||||
@@ -48,6 +48,7 @@ import { NotificationsSettingsV1Component } from "../autofill/popup/settings/not
|
|||||||
import { NotificationsSettingsComponent } from "../autofill/popup/settings/notifications.component";
|
import { NotificationsSettingsComponent } from "../autofill/popup/settings/notifications.component";
|
||||||
import { PremiumComponent } from "../billing/popup/settings/premium.component";
|
import { PremiumComponent } from "../billing/popup/settings/premium.component";
|
||||||
import BrowserPopupUtils from "../platform/popup/browser-popup-utils";
|
import BrowserPopupUtils from "../platform/popup/browser-popup-utils";
|
||||||
|
import { popupRouterCacheGuard } from "../platform/popup/view-cache/popup-router-cache.service";
|
||||||
import { GeneratorComponent } from "../tools/popup/generator/generator.component";
|
import { GeneratorComponent } from "../tools/popup/generator/generator.component";
|
||||||
import { PasswordGeneratorHistoryComponent } from "../tools/popup/generator/password-generator-history.component";
|
import { PasswordGeneratorHistoryComponent } from "../tools/popup/generator/password-generator-history.component";
|
||||||
import { SendAddEditComponent } from "../tools/popup/send/send-add-edit.component";
|
import { SendAddEditComponent } from "../tools/popup/send/send-add-edit.component";
|
||||||
@@ -105,6 +106,7 @@ const routes: Routes = [
|
|||||||
pathMatch: "full",
|
pathMatch: "full",
|
||||||
children: [], // Children lets us have an empty component.
|
children: [], // Children lets us have an empty component.
|
||||||
canActivate: [
|
canActivate: [
|
||||||
|
popupRouterCacheGuard,
|
||||||
redirectGuard({ loggedIn: "/tabs/current", loggedOut: "/home", locked: "/lock" }),
|
redirectGuard({ loggedIn: "/tabs/current", loggedOut: "/home", locked: "/lock" }),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
} from "@bitwarden/components";
|
} from "@bitwarden/components";
|
||||||
|
|
||||||
import { BrowserApi } from "../platform/browser/browser-api";
|
import { BrowserApi } from "../platform/browser/browser-api";
|
||||||
|
import { initPopupClosedListener } from "../platform/services/popup-view-cache-background.service";
|
||||||
import { BrowserSendStateService } from "../tools/popup/services/browser-send-state.service";
|
import { BrowserSendStateService } from "../tools/popup/services/browser-send-state.service";
|
||||||
import { VaultBrowserStateService } from "../vault/services/vault-browser-state.service";
|
import { VaultBrowserStateService } from "../vault/services/vault-browser-state.service";
|
||||||
|
|
||||||
@@ -59,6 +60,8 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
|
initPopupClosedListener();
|
||||||
|
|
||||||
// Component states must not persist between closing and reopening the popup, otherwise they become dead objects
|
// Component states must not persist between closing and reopening the popup, otherwise they become dead objects
|
||||||
// Clear them aggressively to make sure this doesn't occur
|
// Clear them aggressively to make sure this doesn't occur
|
||||||
await this.clearComponentStates();
|
await this.clearComponentStates();
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import { CurrentAccountComponent } from "../../../auth/popup/account-switching/c
|
|||||||
import { PopOutComponent } from "../../../platform/popup/components/pop-out.component";
|
import { PopOutComponent } from "../../../platform/popup/components/pop-out.component";
|
||||||
import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component";
|
import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component";
|
||||||
import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component";
|
import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component";
|
||||||
|
import { PopupRouterCacheService } from "../../../platform/popup/view-cache/popup-router-cache.service";
|
||||||
|
|
||||||
import { SendV2Component, SendState } from "./send-v2.component";
|
import { SendV2Component, SendState } from "./send-v2.component";
|
||||||
|
|
||||||
@@ -102,6 +103,7 @@ describe("SendV2Component", () => {
|
|||||||
{ provide: SendItemsService, useValue: sendItemsService },
|
{ provide: SendItemsService, useValue: sendItemsService },
|
||||||
{ provide: I18nService, useValue: { t: (key: string) => key } },
|
{ provide: I18nService, useValue: { t: (key: string) => key } },
|
||||||
{ provide: SendListFiltersService, useValue: sendListFiltersService },
|
{ provide: SendListFiltersService, useValue: sendListFiltersService },
|
||||||
|
{ provide: PopupRouterCacheService, useValue: mock<PopupRouterCacheService>() },
|
||||||
],
|
],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import { mock, MockProxy } from "jest-mock-extended";
|
|||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { CipherFormGeneratorComponent } from "@bitwarden/vault";
|
import { CipherFormGeneratorComponent } from "@bitwarden/vault";
|
||||||
|
|
||||||
|
import { PopupRouterCacheService } from "../../../../../platform/popup/view-cache/popup-router-cache.service";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
GeneratorDialogParams,
|
GeneratorDialogParams,
|
||||||
GeneratorDialogResult,
|
GeneratorDialogResult,
|
||||||
@@ -39,6 +41,7 @@ describe("VaultGeneratorDialogComponent", () => {
|
|||||||
{ provide: I18nService, useValue: { t: (key: string) => key } },
|
{ provide: I18nService, useValue: { t: (key: string) => key } },
|
||||||
{ provide: DIALOG_DATA, useValue: dialogData },
|
{ provide: DIALOG_DATA, useValue: dialogData },
|
||||||
{ provide: DialogRef, useValue: mockDialogRef },
|
{ provide: DialogRef, useValue: mockDialogRef },
|
||||||
|
{ provide: PopupRouterCacheService, useValue: mock<PopupRouterCacheService>() },
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
.overrideComponent(VaultGeneratorDialogComponent, {
|
.overrideComponent(VaultGeneratorDialogComponent, {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export enum FeatureFlag {
|
|||||||
AC1795_UpdatedSubscriptionStatusSection = "AC-1795_updated-subscription-status-section",
|
AC1795_UpdatedSubscriptionStatusSection = "AC-1795_updated-subscription-status-section",
|
||||||
EnableDeleteProvider = "AC-1218-delete-provider",
|
EnableDeleteProvider = "AC-1218-delete-provider",
|
||||||
ExtensionRefresh = "extension-refresh",
|
ExtensionRefresh = "extension-refresh",
|
||||||
|
PersistPopupView = "persist-popup-view",
|
||||||
RestrictProviderAccess = "restrict-provider-access",
|
RestrictProviderAccess = "restrict-provider-access",
|
||||||
PM4154_BulkEncryptionService = "PM-4154-bulk-encryption-service",
|
PM4154_BulkEncryptionService = "PM-4154-bulk-encryption-service",
|
||||||
UseTreeWalkerApiForPageDetailsCollection = "use-tree-walker-api-for-page-details-collection",
|
UseTreeWalkerApiForPageDetailsCollection = "use-tree-walker-api-for-page-details-collection",
|
||||||
@@ -54,6 +55,7 @@ export const DefaultFeatureFlagValue = {
|
|||||||
[FeatureFlag.AC1795_UpdatedSubscriptionStatusSection]: FALSE,
|
[FeatureFlag.AC1795_UpdatedSubscriptionStatusSection]: FALSE,
|
||||||
[FeatureFlag.EnableDeleteProvider]: FALSE,
|
[FeatureFlag.EnableDeleteProvider]: FALSE,
|
||||||
[FeatureFlag.ExtensionRefresh]: FALSE,
|
[FeatureFlag.ExtensionRefresh]: FALSE,
|
||||||
|
[FeatureFlag.PersistPopupView]: FALSE,
|
||||||
[FeatureFlag.RestrictProviderAccess]: FALSE,
|
[FeatureFlag.RestrictProviderAccess]: FALSE,
|
||||||
[FeatureFlag.PM4154_BulkEncryptionService]: FALSE,
|
[FeatureFlag.PM4154_BulkEncryptionService]: FALSE,
|
||||||
[FeatureFlag.UseTreeWalkerApiForPageDetailsCollection]: FALSE,
|
[FeatureFlag.UseTreeWalkerApiForPageDetailsCollection]: FALSE,
|
||||||
|
|||||||
@@ -111,6 +111,9 @@ export const CRYPTO_MEMORY = new StateDefinition("crypto", "memory");
|
|||||||
export const DESKTOP_SETTINGS_DISK = new StateDefinition("desktopSettings", "disk");
|
export const DESKTOP_SETTINGS_DISK = new StateDefinition("desktopSettings", "disk");
|
||||||
export const ENVIRONMENT_DISK = new StateDefinition("environment", "disk");
|
export const ENVIRONMENT_DISK = new StateDefinition("environment", "disk");
|
||||||
export const ENVIRONMENT_MEMORY = new StateDefinition("environment", "memory");
|
export const ENVIRONMENT_MEMORY = new StateDefinition("environment", "memory");
|
||||||
|
export const POPUP_VIEW_MEMORY = new StateDefinition("popupView", "memory", {
|
||||||
|
browser: "memory-large-object",
|
||||||
|
});
|
||||||
export const SYNC_DISK = new StateDefinition("sync", "disk", { web: "memory" });
|
export const SYNC_DISK = new StateDefinition("sync", "disk", { web: "memory" });
|
||||||
export const THEMING_DISK = new StateDefinition("theming", "disk", { web: "disk-local" });
|
export const THEMING_DISK = new StateDefinition("theming", "disk", { web: "disk-local" });
|
||||||
export const TRANSLATION_DISK = new StateDefinition("translation", "disk", { web: "disk-local" });
|
export const TRANSLATION_DISK = new StateDefinition("translation", "disk", { web: "disk-local" });
|
||||||
|
|||||||
@@ -13,10 +13,6 @@ import { CollectionView } from "@bitwarden/common/vault/models/view/collection.v
|
|||||||
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
||||||
import { SearchModule } from "@bitwarden/components";
|
import { SearchModule } from "@bitwarden/components";
|
||||||
|
|
||||||
import { PopupFooterComponent } from "../../../../apps/browser/src/platform/popup/layout/popup-footer.component";
|
|
||||||
import { PopupHeaderComponent } from "../../../../apps/browser/src/platform/popup/layout/popup-header.component";
|
|
||||||
import { PopupPageComponent } from "../../../../apps/browser/src/platform/popup/layout/popup-page.component";
|
|
||||||
|
|
||||||
import { AdditionalOptionsComponent } from "./additional-options/additional-options.component";
|
import { AdditionalOptionsComponent } from "./additional-options/additional-options.component";
|
||||||
import { AttachmentsV2ViewComponent } from "./attachments/attachments-v2-view.component";
|
import { AttachmentsV2ViewComponent } from "./attachments/attachments-v2-view.component";
|
||||||
import { AutofillOptionsViewComponent } from "./autofill-options/autofill-options-view.component";
|
import { AutofillOptionsViewComponent } from "./autofill-options/autofill-options-view.component";
|
||||||
@@ -35,9 +31,6 @@ import { ViewIdentitySectionsComponent } from "./view-identity-sections/view-ide
|
|||||||
CommonModule,
|
CommonModule,
|
||||||
SearchModule,
|
SearchModule,
|
||||||
JslibModule,
|
JslibModule,
|
||||||
PopupPageComponent,
|
|
||||||
PopupHeaderComponent,
|
|
||||||
PopupFooterComponent,
|
|
||||||
ItemDetailsV2Component,
|
ItemDetailsV2Component,
|
||||||
AdditionalOptionsComponent,
|
AdditionalOptionsComponent,
|
||||||
AttachmentsV2ViewComponent,
|
AttachmentsV2ViewComponent,
|
||||||
|
|||||||
Reference in New Issue
Block a user