Skip to content
Draft
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions apps/browser/src/_locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -5803,6 +5803,9 @@
"settingDisabledByPolicy": {
"message": "This setting is disabled by your organization's policy.",
"description": "This hint text is displayed when a user setting is disabled due to an organization policy."
},
"importPopoutDialogDesc": {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎨 Finding 7: Missing i18n key for dialog title

Only the description key was added, but the template reuses the Send dialog's title (sendFilePopoutDialogText). This should have a dedicated import title key.

Suggested addition:

"importPopoutDialogTitle": {
  "message": "Pop out required"
},
"importPopoutDialogDesc": {
  "message": "To import from a file, you need to pop out the extension to a new window."
}

"message": "To import from a file, you need to pop out the extension to a new window."
},
"zipPostalCodeLabel": {
"message": "ZIP / Postal code"
Expand Down
4 changes: 2 additions & 2 deletions apps/browser/src/popup/app-routing.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ const routes: Routes = [
path: "import",
component: ImportBrowserV2Component,
canActivate: [authGuard],
data: { elevation: 1 } satisfies RouteDataProperties,
data: { elevation: 1, doNotSaveUrl: true } satisfies RouteDataProperties,
},
{
path: "export",
Expand Down Expand Up @@ -324,7 +324,7 @@ const routes: Routes = [
path: "add-send",
component: SendAddEditV2Component,
canActivate: [authGuard],
data: { elevation: 1 } satisfies RouteDataProperties,
data: { elevation: 1, doNotSaveUrl: true } satisfies RouteDataProperties,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: Why are we not saving the url here?

Copy link
Contributor Author

@harr1424 harr1424 Nov 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copying from Claude's comment (below) which correctly notes this logic wasn't applied to the import path:

Without this flag, the import page URL is saved in popup history
If a user navigates away and the popup restores this URL, they could still access the file selector outside the popout
This defeats the purpose of the Firefox crash fix

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💭 Finding 5: Missing route configuration for import path

The add-send route has doNotSaveUrl: true to prevent saving the URL when the popout dialog might be shown. However, there's no corresponding route configuration change for the import page.

Questions:

  1. What is the route path for ImportBrowserV2Component? (Not visible in the diff)
  2. Does that route also need doNotSaveUrl: true for consistency?
  3. Without doNotSaveUrl, users who close the popout dialog will have the import page saved in history, which could lead to the file selector issue recurring

This addresses the reviewer's question about why we're not saving the URL here.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Finding 8: Import route missing doNotSaveUrl: true flag

The import route (line 246-249) should also have data: { elevation: 1, doNotSaveUrl: true } for consistency with the Send route change.

Why this matters:

  • Without this flag, the import page URL is saved in popup history
  • If a user navigates away and the popup restores this URL, they could still access the file selector outside the popout
  • This defeats the purpose of the Firefox crash fix

Required change:

{
  path: "import",
  component: ImportBrowserV2Component,
  canActivate: [authGuard],
  data: { elevation: 1, doNotSaveUrl: true } satisfies RouteDataProperties,
}

},
{
path: "edit-send",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { CommonModule } from "@angular/common";
import { Component } from "@angular/core";
import { Router } from "@angular/router";

import { JslibModule } from "@bitwarden/angular/jslib.module";
import { ButtonModule, DialogModule, DialogService, TypographyModule } from "@bitwarden/components";
Expand All @@ -14,13 +15,18 @@ import BrowserPopupUtils from "../../../../platform/browser/browser-popup-utils"
imports: [JslibModule, CommonModule, DialogModule, ButtonModule, TypographyModule],
})
export class SendFilePopoutDialogComponent {
constructor(private dialogService: DialogService) {}
constructor(
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Finding 4: Use inject() instead of constructor injection

Suggested change
constructor(
private dialogService = inject(DialogService);
private router = inject(Router);

private dialogService: DialogService,
private router: Router,
) {}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use inject instead of constructor

async popOutWindow() {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Finding 5: Missing return type annotation

Suggested change
async popOutWindow() {
async popOutWindow(): Promise<void> {

await BrowserPopupUtils.openCurrentPagePopout(window);
}

close() {
async close() {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return type

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Finding 5: Missing return type annotation

Suggested change
async close() {
async close(): Promise<void> {

this.dialogService.closeAll();
// the current view exposes a file selector, ensure the view is popped to avoid using it outside of a popout
await this.router.navigate(["/tabs/send"]);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Finding 3: Inconsistent navigation patterns create technical debt

The Send dialog uses Router.navigate() to an absolute path while the Import dialog uses popupRouterCacheService.back(). This inconsistency creates confusion and maintenance burden.

Issue: Both components solve the same problem (prevent file selector outside popout) but use different navigation strategies.

Recommendation: Standardize on one approach:

  • Option A: Both use explicit navigation to a known route
  • Option B: Both use the router cache service (requires refactoring Send)
  • Option C: Extract shared navigation logic to FilePopoutUtilsService

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
>
</tools-import>

<import-file-popout-dialog-container></import-file-popout-dialog-container>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: Why is this a separate component rather than just living inside the import-browser component? It seems quite weird to have a child component popup a dialog that redirects the parent page.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💭 Finding 4: Component placement and responsibilities need clarification

The <import-file-popout-dialog-container> is embedded in the template but has no visual representation (empty template). This creates unclear component boundaries.

Questions for consideration:

  1. Why is this a separate component rather than logic in the parent ImportBrowserV2Component?
  2. The container component opens a dialog that navigates the parent - this violates typical component encapsulation
  3. Should this logic live in a route guard instead, since it's checking conditions on route entry?

Alternative approaches:

  • Move the dialog-opening logic to ImportBrowserV2Component.ngOnInit()
  • Or use a route guard that opens the dialog before route activation
  • Or keep as child component but have it emit events rather than directly navigating

This addresses the reviewer's question about architecture.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💭 Finding 2: Component architecture violates encapsulation principles

This container component has no visual output (empty template) but opens a dialog that navigates the parent page, creating unclear boundaries.

Questions:

  1. Why is this a separate component rather than logic in ImportBrowserV2Component.ngOnInit()?
  2. Should this be a route guard instead since it checks conditions on route entry?

Alternative: Move dialog logic directly to parent component or implement as canActivate guard.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dont need closing tag

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Finding 6: Unnecessary closing tag

Self-closing components don't need closing tags in Angular.

Suggested change
<import-file-popout-dialog-container></import-file-popout-dialog-container>
<import-file-popout-dialog-container />


<popup-footer slot="footer">
<button
[disabled]="disabled"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import { PopupFooterComponent } from "../../../../platform/popup/layout/popup-fo
import { PopupHeaderComponent } from "../../../../platform/popup/layout/popup-header.component";
import { PopupPageComponent } from "../../../../platform/popup/layout/popup-page.component";

import { ImportFilePopoutDialogContainerComponent } from "./import-file-popout-dialog-container.component";

// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
@Component({
Expand All @@ -35,6 +37,7 @@ import { PopupPageComponent } from "../../../../platform/popup/layout/popup-page
PopupFooterComponent,
PopupHeaderComponent,
PopOutComponent,
ImportFilePopoutDialogContainerComponent,
],
providers: [
...ImporterProviders,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { CommonModule } from "@angular/common";
import { Component, OnInit, ChangeDetectionStrategy, OnDestroy } from "@angular/core";

import { JslibModule } from "@bitwarden/angular/jslib.module";
import { DialogRef, DialogService } from "@bitwarden/components";

import { FilePopoutUtilsService } from "../../services/file-popout-utils.service";

import { ImportFilePopoutDialogComponent } from "./import-file-popout-dialog.component";

@Component({
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Finding 3: Component should be standalone

New Angular components should include standalone: true per project modernization efforts.

Suggested change
@Component({
@Component({
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,

changeDetection: ChangeDetectionStrategy.OnPush,
selector: "import-file-popout-dialog-container",
template: "",
imports: [JslibModule, CommonModule],
})
export class ImportFilePopoutDialogContainerComponent implements OnInit, OnDestroy {
private dialogRef: DialogRef | null = null;

constructor(
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Finding 4: Use inject() instead of constructor injection

Per Angular modernization patterns, prefer inject():

Suggested change
constructor(
private dialogService = inject(DialogService);
private filePopoutUtilsService = inject(FilePopoutUtilsService);

private dialogService: DialogService,
private filePopoutUtilsService: FilePopoutUtilsService,
) {}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

inject


ngOnInit() {
if (this.filePopoutUtilsService.showFilePopoutMessage(window)) {
this.dialogRef = this.dialogService.open(ImportFilePopoutDialogComponent);
}
}

ngOnDestroy() {
this.dialogRef?.close();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<bit-simple-dialog dialogSize="default">
<div bitDialogIcon>
<i class="bwi bwi-info-circle bwi-2x tw-text-info" aria-hidden="true"></i>
</div>
<ng-container bitDialogContent>
<div bitTypography="h3">
{{ "sendFilePopoutDialogText" | i18n }}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Finding 6: Incorrect i18n key reuse creates confusing UX

Line 7 uses "sendFilePopoutDialogText" (Send-related) while line 9 correctly uses "importPopoutDialogDesc" (Import-related). This creates a confusing message that says "Send file" in the title but talks about importing in the description.

Expected behavior: The title should use an import-specific i18n key.

Fix needed:

<div bitTypography="h3">
  {{ "importPopoutDialogTitle" | i18n }}
</div>

Then add to messages.json:

"importPopoutDialogTitle": {
  "message": "Pop out required"
}

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Finding 1: Incorrect i18n key creates confusing dialog title

This uses sendFilePopoutDialogText which displays "Pop out extension?" with a description about Send. However, line 9 correctly uses importPopoutDialogDesc for the description, creating inconsistent messaging.

Required fix:

Suggested change
{{ "sendFilePopoutDialogText" | i18n }}
{{ "importPopoutDialogTitle" | i18n }}

Then add to messages.json:

"importPopoutDialogTitle": {
  "message": "Pop out to import"
}

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Finding 1: Wrong i18n key creates confusing dialog title

This uses sendFilePopoutDialogText which displays "Pop out extension?" (a Send-specific message), but line 9 correctly uses importPopoutDialogDesc for the description. This creates inconsistent messaging.

Required fix:

Suggested change
{{ "sendFilePopoutDialogText" | i18n }}
{{ "importPopoutDialogTitle" | i18n }}

Then add to messages.json:

"importPopoutDialogTitle": {
  "message": "Pop out to import"
}

</div>
<div bitTypography="body1">{{ "importPopoutDialogDesc" | i18n }}</div>
</ng-container>
<ng-container bitDialogFooter>
<button buttonType="primary" bitButton type="button" (click)="popOutWindow()">
{{ "popOut" | i18n }}
<i class="bwi bwi-popout tw-ml-1" aria-hidden="true"></i>
</button>
<button bitButton buttonType="secondary" type="button" (click)="close()">
{{ "cancel" | i18n }}
</button>
</ng-container>
</bit-simple-dialog>
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { CommonModule } from "@angular/common";
import { Component, ChangeDetectionStrategy } from "@angular/core";

import { JslibModule } from "@bitwarden/angular/jslib.module";
import { ButtonModule, DialogModule, DialogService, TypographyModule } from "@bitwarden/components";

import BrowserPopupUtils from "../../../../platform/browser/browser-popup-utils";
import { PopupRouterCacheService } from "../../../../platform/popup/view-cache/popup-router-cache.service";

@Component({
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Finding 3: Component should be standalone

Suggested change
@Component({
@Component({
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,

changeDetection: ChangeDetectionStrategy.OnPush,
selector: "import-file-popout-dialog",
templateUrl: "./import-file-popout-dialog.component.html",
imports: [JslibModule, CommonModule, DialogModule, ButtonModule, TypographyModule],
})

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

standalone component

export class ImportFilePopoutDialogComponent {
constructor(
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Finding 4: Use inject() instead of constructor injection

Suggested change
constructor(
private dialogService = inject(DialogService);
private popupRouterCacheService = inject(PopupRouterCacheService);

private dialogService: DialogService,
private popupRouterCacheService: PopupRouterCacheService,
) {}

async popOutWindow() {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Finding 5: Missing return type annotation

Suggested change
async popOutWindow() {
async popOutWindow(): Promise<void> {

await BrowserPopupUtils.openCurrentPagePopout(window);
}

async close() {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Finding 5: Missing return type annotation

Suggested change
async close() {
async close(): Promise<void> {

this.dialogService.closeAll();
// the current view exposes a file selector, ensure the view is popped to avoid using it outside of a popout
await this.popupRouterCacheService.back();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Finding 1: Navigation logic may fail silently if back() has no history

The popupRouterCacheService.back() method will navigate to the root vault page if history is empty. This could be confusing UX if the user arrived at the import page through a direct link or bookmark rather than navigation.

Consider:

  1. Explicitly navigating to a known safe route (e.g., /vault-settings) instead of relying on back()
  2. Or add error handling if back() returns false or fails
Context from PopupRouterCacheService
async back() {
  // ...
  if (this.hasNavigated && history.length) {
    this.location.back();
    return;
  }
  // if no history is present, fallback to vault page
  await this.router.navigate([""]);
}

This fallback behavior may not match user expectations when canceling an import.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Finding 3: Inconsistent navigation patterns create technical debt

Import uses popupRouterCacheService.back() while Send uses Router.navigate(["/tabs/send"]) (send-file-popout-dialog.component.ts:30). Both solve the same problem with different approaches.

Recommendation: Standardize navigation strategy across both features or extract shared logic to FilePopoutUtilsService.

}
}
Loading